Merge branch 'feature/ACK' into develop

This commit is contained in:
Christian Basler 2016-05-24 19:36:41 +02:00
commit 3d2cea91ce
62 changed files with 1448 additions and 569 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
### Gradle ### ### Gradle ###
.gradle .gradle
build/ build/
classes/
# Ignore Gradle GUI config # Ignore Gradle GUI config
gradle-app.setting gradle-app.setting

View File

@ -5,10 +5,11 @@ Jabit
[![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE) [![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE)
[![Visit our IRC channel](https://img.shields.io/badge/irc-%23jabit-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#jabit) [![Visit our IRC channel](https://img.shields.io/badge/irc-%23jabit-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#jabit)
A Java implementation for the Bitmessage protocol. To build, use command `gradle build` or `./gradlew build`. A Java implementation for the Bitmessage protocol. To build, use command `./gradlew build`.
Please note that it still has its limitations, but the API should now be stable. Jabit uses Semantic Versioning, meaning Please note that it still has its limitations, but the API should now be stable. Jabit uses Semantic Versioning, meaning as long as the major version doesn't change, nothing should break if you update.
as long as the major version doesn't change, nothing should break if you update.
Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch, notably when it comes to database updates. _In other words, they may break your installation!_
#### Master #### Master
[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) [![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit)
@ -23,9 +24,7 @@ as long as the major version doesn't change, nothing should break if you update.
Security Security
-------- --------
There are most probably some security issues, me programming this thing all by myself. Jabit doesn't do anything against There are most probably some security issues, me programming this thing all by myself. Jabit doesn't do anything against timing attacks yet, for example. Please feel free to use the library, report bugs and maybe even help out. I hope the code is easy to understand and work with.
timing attacks yet, for example. Please feel free to use the library, report bugs and maybe even help out. I hope the
code is easy to understand and work with.
Project Status Project Status
-------------- --------------
@ -74,17 +73,22 @@ BitmessageContext ctx = new BitmessageContext.Builder()
.nodeRegistry(new MemoryNodeRegistry()) .nodeRegistry(new MemoryNodeRegistry())
.networkHandler(new NetworkNode()) .networkHandler(new NetworkNode())
.cryptography(new BouncyCryptography()) .cryptography(new BouncyCryptography())
.listener(System.out::println)
.build(); .build();
``` ```
This creates a simple context using a H2 database that will be created in the user's home directory. Next you'll need to This creates a simple context using a H2 database that will be created in the user's home directory. In the listener you decide what happens when a message arrives. If you can't use lambdas, you may instead write
start the context and decide what happens if a message arrives:
```Java ```Java
ctx.startup(new BitmessageContext.Listener() { .listener(new BitmessageContext.Listener() {
@Override @Override
public void receive(Plaintext plaintext) { public void receive(Plaintext plaintext) {
// TODO: Notify the user // TODO: Notify the user
} }
}); })
```
Next you'll need to start the context:
```Java
ctx.startup()
``` ```
Then you might want to create an identity Then you might want to create an identity
```Java ```Java
@ -100,3 +104,22 @@ to which you can send some messages
```Java ```Java
ctx.send(identity, contact, "Test", "Hello Chris, this is a message."); ctx.send(identity, contact, "Test", "Hello Chris, this is a message.");
``` ```
### Housekeeping
As Bitmessage stores all currently valid messages, we'll need to delete expired objects from time to time:
```Java
ctx.cleanup();
```
If the client runs all the time, it might be a good idea to do this daily or at least weekly. Otherwise, you might just want to clean up on shutdown.
Also, if some messages weren't acknowledged when it expired, they can be resent:
```Java
ctx.resendUnacknowledgedMessages();
```
This could be triggered periodically, or manually by the user. Please be aware that _if_ there is a message to resend, proof of work needs to be calculated, so to not annoy your users you might not want to trigger it on shutdown. As the client might have been offline for some time, it might as well be wise to wait until it caught up downloading new messages before resending those messages, after all they might be acknowledged by now.
There probably won't happen extremely bad things if you don't - at least not more than otherwise - but you can properly shutdown the network connection by calling
```Java
ctx.shutdown();
```

View File

@ -26,6 +26,7 @@ artifacts {
dependencies { dependencies {
compile 'org.slf4j:slf4j-api:1.7.12' compile 'org.slf4j:slf4j-api:1.7.12'
testCompile 'junit:junit:4.11' testCompile 'junit:junit:4.11'
testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'
testCompile project(':cryptography-bc') testCompile project(':cryptography-bc')
} }

View File

@ -1,47 +0,0 @@
/*
* Copyright 2016 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;
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
/**
* Default implementation that doesn't do anything.
*
* @author Christian Basler
*/
public class BaseMessageCallback implements MessageCallback {
@Override
public void proofOfWorkStarted(ObjectPayload message) {
// No op
}
@Override
public void proofOfWorkCompleted(ObjectPayload message) {
// No op
}
@Override
public void messageOffered(ObjectPayload message, InventoryVector iv) {
// No op
}
@Override
public void messageAcknowledged(InventoryVector iv) {
// No op
}
}

View File

@ -18,13 +18,9 @@ package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*; import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.Broadcast; import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.ports.*;
@ -35,15 +31,12 @@ import org.slf4j.LoggerFactory;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.List; import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES;
import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE;
import static ch.dissem.bitmessage.entity.Plaintext.Status.*;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.UnixTime.*; import static ch.dissem.bitmessage.utils.UnixTime.*;
@ -76,16 +69,10 @@ public class BitmessageContext {
private BitmessageContext(Builder builder) { private BitmessageContext(Builder builder) {
ctx = new InternalContext(builder); ctx = new InternalContext(builder);
labeler = builder.labeler; labeler = builder.labeler;
ctx.getProofOfWorkService().doMissingProofOfWork(30_000); // TODO: this should be configurable
networkListener = new DefaultMessageListener(ctx, labeler, builder.listener); networkListener = new DefaultMessageListener(ctx, labeler, builder.listener);
sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation; sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
ctx.getProofOfWorkService().doMissingProofOfWork();
}
}, 30_000); // After 30 seconds
} }
public AddressRepository addresses() { public AddressRepository addresses() {
@ -157,7 +144,6 @@ public class BitmessageContext {
.from(from) .from(from)
.to(to) .to(to)
.message(subject, message) .message(subject, message)
.labels(messages().getLabels(Label.Type.SENT))
.build(); .build();
send(msg); send(msg);
} }
@ -166,6 +152,7 @@ public class BitmessageContext {
if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) { if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) {
throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key.");
} }
labeler().markAsSending(msg);
BitmessageAddress to = msg.getTo(); BitmessageAddress to = msg.getTo();
if (to != null) { if (to != null) {
if (to.getPubkey() == null) { if (to.getPubkey() == null) {
@ -173,36 +160,23 @@ public class BitmessageContext {
ctx.requestPubkey(to); ctx.requestPubkey(to);
} }
if (to.getPubkey() == null) { if (to.getPubkey() == null) {
msg.setStatus(PUBKEY_REQUESTED);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX));
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
} }
} }
if (to == null || to.getPubkey() != null) { if (to == null || to.getPubkey() != null) {
LOG.info("Sending message."); LOG.info("Sending message.");
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
if (msg.getType() == MSG) {
ctx.send(msg);
} else {
ctx.send( ctx.send(
msg.getFrom(), msg.getFrom(),
to, to,
wrapInObjectPayload(msg), Factory.getBroadcast(msg),
TTL.msg() msg.getTTL()
); );
msg.setStatus(SENT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT));
ctx.getMessageRepository().save(msg);
} }
} }
private ObjectPayload wrapInObjectPayload(Plaintext msg) {
switch (msg.getType()) {
case MSG:
return new Msg(msg);
case BROADCAST:
return Factory.getBroadcast(msg);
default:
throw new ApplicationException("Unknown message type " + msg.getType());
}
} }
public void startup() { public void startup() {
@ -247,10 +221,32 @@ public class BitmessageContext {
return ctx.getNetworkHandler().send(server, port, request); return ctx.getNetworkHandler().send(server, port, request);
} }
/**
* Removes expired objects from the inventory. You should call this method regularly,
* e.g. daily and on each shutdown.
*/
public void cleanup() { public void cleanup() {
ctx.getInventory().cleanup(); ctx.getInventory().cleanup();
} }
/**
* Sends messages again whose time to live expired without being acknowledged. (And whose
* recipient is expected to send acknowledgements.
* <p>
* You should call this method regularly, but be aware of the following:
* <ul>
* <li>As messages might be sent, POW will be done. It is therefore not advised to
* call it on shutdown.</li>
* <li>It shouldn't be called right after startup, as it's possible the missing
* acknowledgement was sent while the client was offline.</li>
* <li>Other than that, the call isn't expensive as long as there is no message
* to send, so it might be a good idea to just call it every few minutes.</li>
* </ul>
*/
public void resendUnacknowledgedMessages() {
ctx.resendUnacknowledged();
}
public boolean isRunning() { public boolean isRunning() {
return ctx.getNetworkHandler().isRunning(); return ctx.getNetworkHandler().isRunning();
} }
@ -285,7 +281,8 @@ public class BitmessageContext {
public Property status() { public Property status() {
return new Property("status", null, return new Property("status", null,
ctx.getNetworkHandler().getNetworkStatus() ctx.getNetworkHandler().getNetworkStatus(),
new Property("unacknowledged", ctx.getMessageRepository().findMessagesToResend().size())
); );
} }
@ -311,7 +308,6 @@ public class BitmessageContext {
ProofOfWorkRepository proofOfWorkRepository; ProofOfWorkRepository proofOfWorkRepository;
ProofOfWorkEngine proofOfWorkEngine; ProofOfWorkEngine proofOfWorkEngine;
Cryptography cryptography; Cryptography cryptography;
MessageCallback messageCallback;
CustomCommandHandler customCommandHandler; CustomCommandHandler customCommandHandler;
Labeler labeler; Labeler labeler;
Listener listener; Listener listener;
@ -359,11 +355,6 @@ public class BitmessageContext {
return this; return this;
} }
public Builder messageCallback(MessageCallback callback) {
this.messageCallback = callback;
return this;
}
public Builder customCommandHandler(CustomCommandHandler handler) { public Builder customCommandHandler(CustomCommandHandler handler) {
this.customCommandHandler = handler; this.customCommandHandler = handler;
return this; return this;
@ -430,9 +421,6 @@ public class BitmessageContext {
if (proofOfWorkEngine == null) { if (proofOfWorkEngine == null) {
proofOfWorkEngine = new MultiThreadedPOWEngine(); proofOfWorkEngine = new MultiThreadedPOWEngine();
} }
if (messageCallback == null) {
messageCallback = new BaseMessageCallback();
}
if (labeler == null) { if (labeler == null) {
labeler = new DefaultLabeler(); labeler = new DefaultLabeler();
} }

View File

@ -24,6 +24,7 @@ import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.ports.Labeler; import ch.dissem.bitmessage.ports.Labeler;
import ch.dissem.bitmessage.ports.NetworkHandler; import ch.dissem.bitmessage.ports.NetworkHandler;
import ch.dissem.bitmessage.utils.TTL;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -47,9 +48,15 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
} }
@Override @Override
@SuppressWarnings("ConstantConditions")
public void receive(ObjectMessage object) throws IOException { public void receive(ObjectMessage object) throws IOException {
ObjectPayload payload = object.getPayload(); ObjectPayload payload = object.getPayload();
if (payload.getType() == null) return; if (payload.getType() == null) {
if (payload instanceof GenericPayload) {
receive((GenericPayload) payload);
}
return;
}
switch (payload.getType()) { switch (payload.getType()) {
case GET_PUBKEY: { case GET_PUBKEY: {
@ -109,16 +116,9 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
List<Plaintext> messages = ctx.getMessageRepository().findMessages(PUBKEY_REQUESTED, address); List<Plaintext> messages = ctx.getMessageRepository().findMessages(PUBKEY_REQUESTED, address);
LOG.info("Sending " + messages.size() + " messages for contact " + address); LOG.info("Sending " + messages.size() + " messages for contact " + address);
for (Plaintext msg : messages) { for (Plaintext msg : messages) {
msg.setStatus(DOING_PROOF_OF_WORK); ctx.getLabeler().markAsSending(msg);
ctx.getMessageRepository().save(msg);
ctx.send(
msg.getFrom(),
msg.getTo(),
new Msg(msg),
+2 * DAY
);
msg.setStatus(SENT);
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
ctx.send(msg);
} }
} }
@ -126,11 +126,12 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) { for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) {
try { try {
msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey()); msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey());
msg.getPlaintext().setTo(identity); Plaintext plaintext = msg.getPlaintext();
if (!object.isSignatureValid(msg.getPlaintext().getFrom().getPubkey())) { plaintext.setTo(identity);
if (!object.isSignatureValid(plaintext.getFrom().getPubkey())) {
LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
} else { } else {
receive(object.getInventoryVector(), msg.getPlaintext()); receive(object.getInventoryVector(), plaintext);
} }
break; break;
} catch (DecryptionFailedException ignore) { } catch (DecryptionFailedException ignore) {
@ -138,6 +139,16 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
} }
} }
protected void receive(GenericPayload ack) {
if (ack.getData().length == Msg.ACK_LENGTH) {
Plaintext msg = ctx.getMessageRepository().getMessageForAck(ack.getData());
if (msg != null) {
ctx.getLabeler().markAsAcknowledged(msg);
ctx.getMessageRepository().save(msg);
}
}
}
protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException { protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException {
byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null; byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null;
for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) { for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) {
@ -157,11 +168,18 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
} }
protected void receive(InventoryVector iv, Plaintext msg) { protected void receive(InventoryVector iv, Plaintext msg) {
msg.setStatus(RECEIVED);
msg.setInventoryVector(iv); msg.setInventoryVector(iv);
labeler.setLabels(msg); labeler.setLabels(msg);
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
listener.receive(msg); listener.receive(msg);
updatePubkey(msg.getFrom(), msg.getFrom().getPubkey()); updatePubkey(msg.getFrom(), msg.getFrom().getPubkey());
if (msg.getType() == Plaintext.Type.MSG && msg.getTo().has(Pubkey.Feature.DOES_ACK)) {
ObjectMessage ack = msg.getAckMessage();
if (ack != null) {
ctx.getInventory().storeObject(ack);
ctx.getNetworkHandler().offer(ack.getInventoryVector());
}
}
} }
} }

View File

@ -16,9 +16,7 @@
package ch.dissem.bitmessage; package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.ports.*;
@ -30,6 +28,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.TreeSet; import java.util.TreeSet;
/** /**
@ -54,9 +53,9 @@ public class InternalContext {
private final MessageRepository messageRepository; private final MessageRepository messageRepository;
private final ProofOfWorkRepository proofOfWorkRepository; private final ProofOfWorkRepository proofOfWorkRepository;
private final ProofOfWorkEngine proofOfWorkEngine; private final ProofOfWorkEngine proofOfWorkEngine;
private final MessageCallback messageCallback;
private final CustomCommandHandler customCommandHandler; private final CustomCommandHandler customCommandHandler;
private final ProofOfWorkService proofOfWorkService; private final ProofOfWorkService proofOfWorkService;
private final Labeler labeler;
private final TreeSet<Long> streams = new TreeSet<>(); private final TreeSet<Long> streams = new TreeSet<>();
private final int port; private final int port;
@ -75,11 +74,11 @@ public class InternalContext {
this.proofOfWorkService = new ProofOfWorkService(); this.proofOfWorkService = new ProofOfWorkService();
this.proofOfWorkEngine = builder.proofOfWorkEngine; this.proofOfWorkEngine = builder.proofOfWorkEngine;
this.clientNonce = cryptography.randomNonce(); this.clientNonce = cryptography.randomNonce();
this.messageCallback = builder.messageCallback;
this.customCommandHandler = builder.customCommandHandler; this.customCommandHandler = builder.customCommandHandler;
this.port = builder.port; this.port = builder.port;
this.connectionLimit = builder.connectionLimit; this.connectionLimit = builder.connectionLimit;
this.connectionTTL = builder.connectionTTL; this.connectionTTL = builder.connectionTTL;
this.labeler = builder.labeler;
Singleton.initialize(cryptography); Singleton.initialize(cryptography);
@ -95,8 +94,7 @@ public class InternalContext {
} }
init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, customCommandHandler, builder.labeler);
messageCallback, customCommandHandler, builder.labeler);
for (BitmessageAddress identity : addressRepository.getIdentities()) { for (BitmessageAddress identity : addressRepository.getIdentities()) {
streams.add(identity.getStream()); streams.add(identity.getStream());
} }
@ -146,6 +144,10 @@ public class InternalContext {
return proofOfWorkService; return proofOfWorkService;
} }
public Labeler getLabeler() {
return labeler;
}
public long[] getStreams() { public long[] getStreams() {
long[] result = new long[streams.size()]; long[] result = new long[streams.size()];
int i = 0; int i = 0;
@ -159,14 +161,24 @@ public class InternalContext {
return port; return port;
} }
public void send(final Plaintext plaintext) {
if (plaintext.getAckMessage() != null) {
long expires = UnixTime.now(+plaintext.getTTL());
LOG.info("Expires at " + expires);
proofOfWorkService.doProofOfWorkWithAck(plaintext, expires);
} else {
send(plaintext.getFrom(), plaintext.getTo(), new Msg(plaintext), plaintext.getTTL());
}
}
public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload, public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload,
final long timeToLive) { final long timeToLive) {
try { try {
if (to == null) to = from; final BitmessageAddress recipient = (to != null ? to : from);
long expires = UnixTime.now(+timeToLive); long expires = UnixTime.now(+timeToLive);
LOG.info("Expires at " + expires); LOG.info("Expires at " + expires);
final ObjectMessage object = new ObjectMessage.Builder() final ObjectMessage object = new ObjectMessage.Builder()
.stream(to.getStream()) .stream(recipient.getStream())
.expiresTime(expires) .expiresTime(expires)
.payload(payload) .payload(payload)
.build(); .build();
@ -176,9 +188,8 @@ public class InternalContext {
if (payload instanceof Broadcast) { if (payload instanceof Broadcast) {
((Broadcast) payload).encrypt(); ((Broadcast) payload).encrypt();
} else if (payload instanceof Encrypted) { } else if (payload instanceof Encrypted) {
object.encrypt(to.getPubkey()); object.encrypt(recipient.getPubkey());
} }
messageCallback.proofOfWorkStarted(payload);
proofOfWorkService.doProofOfWork(to, object); proofOfWorkService.doProofOfWork(to, object);
} catch (IOException e) { } catch (IOException e) {
throw new ApplicationException(e); throw new ApplicationException(e);
@ -196,7 +207,6 @@ public class InternalContext {
.build(); .build();
response.sign(identity.getPrivateKey()); response.sign(identity.getPrivateKey());
response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey())); response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey()));
messageCallback.proofOfWorkStarted(identity.getPubkey());
// TODO: remember that the pubkey is just about to be sent, and on which stream! // TODO: remember that the pubkey is just about to be sent, and on which stream!
proofOfWorkService.doProofOfWork(response); proofOfWorkService.doProofOfWork(response);
} catch (IOException e) { } catch (IOException e) {
@ -233,7 +243,6 @@ public class InternalContext {
.expiresTime(expires) .expiresTime(expires)
.payload(new GetPubkey(contact)) .payload(new GetPubkey(contact))
.build(); .build();
messageCallback.proofOfWorkStarted(request.getPayload());
proofOfWorkService.doProofOfWork(request); proofOfWorkService.doProofOfWork(request);
} }
@ -271,6 +280,14 @@ public class InternalContext {
} }
} }
public void resendUnacknowledged() {
List<Plaintext> messages = messageRepository.findMessagesToResend();
for (Plaintext message : messages) {
send(message);
messageRepository.save(message);
}
}
public long getClientNonce() { public long getClientNonce() {
return clientNonce; return clientNonce;
} }

View File

@ -1,52 +0,0 @@
/*
* 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;
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
/**
* Callback for message sending events, mostly so the user can be notified when POW is done.
*/
public interface MessageCallback {
/**
* Called before calculation of proof of work begins.
*/
void proofOfWorkStarted(ObjectPayload message);
/**
* Called after calculation of proof of work finished.
*/
void proofOfWorkCompleted(ObjectPayload message);
/**
* Called once the message is offered to the network. Please note that this doesn't mean the message was sent,
* if the client is not connected to the network it's just stored in the inventory.
* <p>
* Also, please note that this is where the original payload as well as the {@link InventoryVector} of the sent
* message is available. If the callback needs the IV for some reason, it should be retrieved here. (Plaintext
* and Broadcast messages will have their IV property set automatically though.)
* </p>
*/
void messageOffered(ObjectPayload message, InventoryVector iv);
/**
* This isn't called yet, as ACK messages aren't being processed yet. Also, this is only relevant for Plaintext
* messages.
*/
void messageAcknowledged(InventoryVector iv);
}

View File

@ -1,22 +1,23 @@
package ch.dissem.bitmessage; package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.ports.Cryptography; import ch.dissem.bitmessage.ports.Cryptography;
import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES;
import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* @author Christian Basler * @author Christian Basler
@ -29,16 +30,23 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC
private ProofOfWorkRepository powRepo; private ProofOfWorkRepository powRepo;
private MessageRepository messageRepo; private MessageRepository messageRepo;
public void doMissingProofOfWork() { public void doMissingProofOfWork(long delayInMilliseconds) {
List<byte[]> items = powRepo.getItems(); final List<byte[]> items = powRepo.getItems();
if (items.isEmpty()) return; if (items.isEmpty()) return;
// Wait for 30 seconds, to let the application start up before putting heavy load on the CPU
new Timer().schedule(new TimerTask() {
@Override
public void run() {
LOG.info("Doing POW for " + items.size() + " tasks."); LOG.info("Doing POW for " + items.size() + " tasks.");
for (byte[] initialHash : items) { for (byte[] initialHash : items) {
ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); Item item = powRepo.getItem(initialHash);
cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, this); cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes,
ProofOfWorkService.this);
} }
} }
}, delayInMilliseconds);
}
public void doProofOfWork(ObjectMessage object) { public void doProofOfWork(ObjectMessage object) {
doProofOfWork(null, object); doProofOfWork(null, object);
@ -59,24 +67,52 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC
cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this); cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this);
} }
public void doProofOfWorkWithAck(Plaintext plaintext, long expirationTime) {
final ObjectMessage ack = plaintext.getAckMessage();
messageRepo.save(plaintext);
Item item = new Item(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES,
expirationTime, plaintext);
powRepo.putObject(item);
cryptography.doProofOfWork(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, this);
}
@Override @Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) { public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
Item item = powRepo.getItem(initialHash);
if (item.message == null) {
ObjectMessage object = powRepo.getItem(initialHash).object; ObjectMessage object = powRepo.getItem(initialHash).object;
object.setNonce(nonce); object.setNonce(nonce);
Plaintext plaintext = messageRepo.getMessage(initialHash); Plaintext plaintext = messageRepo.getMessage(initialHash);
if (plaintext != null) { if (plaintext != null) {
plaintext.setInventoryVector(object.getInventoryVector()); plaintext.setInventoryVector(object.getInventoryVector());
plaintext.updateNextTry();
ctx.getLabeler().markAsSent(plaintext);
messageRepo.save(plaintext); messageRepo.save(plaintext);
} }
ctx.getInventory().storeObject(object); ctx.getInventory().storeObject(object);
powRepo.removeObject(initialHash);
ctx.getNetworkHandler().offer(object.getInventoryVector()); ctx.getNetworkHandler().offer(object.getInventoryVector());
} else {
item.message.getAckMessage().setNonce(nonce);
final ObjectMessage object = new ObjectMessage.Builder()
.stream(item.message.getStream())
.expiresTime(item.expirationTime)
.payload(new Msg(item.message))
.build();
if (object.isSigned()) {
object.sign(item.message.getFrom().getPrivateKey());
}
if (object.getPayload() instanceof Encrypted) {
object.encrypt(item.message.getTo().getPubkey());
}
doProofOfWork(item.message.getTo(), object);
}
powRepo.removeObject(initialHash);
} }
@Override @Override
public void setContext(InternalContext ctx) { public void setContext(InternalContext ctx) {
this.ctx = ctx; this.ctx = ctx;
this.cryptography = security(); this.cryptography = cryptography();
this.powRepo = ctx.getProofOfWorkRepository(); this.powRepo = ctx.getProofOfWorkRepository();
this.messageRepo = ctx.getMessageRepository(); this.messageRepo = ctx.getMessageRepository();
} }

View File

@ -17,6 +17,7 @@
package ch.dissem.bitmessage.entity; package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.ApplicationException;
@ -36,7 +37,7 @@ import java.util.Objects;
import static ch.dissem.bitmessage.utils.Decode.bytes; import static ch.dissem.bitmessage.utils.Decode.bytes;
import static ch.dissem.bitmessage.utils.Decode.varInt; import static ch.dissem.bitmessage.utils.Decode.varInt;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address * A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address
@ -73,19 +74,19 @@ public class BitmessageAddress implements Serializable {
Encode.varInt(version, os); Encode.varInt(version, os);
Encode.varInt(stream, os); Encode.varInt(stream, os);
if (version < 4) { if (version < 4) {
byte[] checksum = security().sha512(os.toByteArray(), ripe); byte[] checksum = cryptography().sha512(os.toByteArray(), ripe);
this.tag = null; this.tag = null;
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} else { } else {
// for tag and decryption key, the checksum has to be created with 0x00 padding // for tag and decryption key, the checksum has to be created with 0x00 padding
byte[] checksum = security().doubleSha512(os.toByteArray(), ripe); byte[] checksum = cryptography().doubleSha512(os.toByteArray(), ripe);
this.tag = Arrays.copyOfRange(checksum, 32, 64); this.tag = Arrays.copyOfRange(checksum, 32, 64);
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} }
// but for the address and its checksum they need to be stripped // but for the address and its checksum they need to be stripped
int offset = Bytes.numberOfLeadingZeros(ripe); int offset = Bytes.numberOfLeadingZeros(ripe);
os.write(ripe, offset, ripe.length - offset); os.write(ripe, offset, ripe.length - offset);
byte[] checksum = security().doubleSha512(os.toByteArray()); byte[] checksum = cryptography().doubleSha512(os.toByteArray());
os.write(checksum, 0, 4); os.write(checksum, 0, 4);
this.address = "BM-" + Base58.encode(os.toByteArray()); this.address = "BM-" + Base58.encode(os.toByteArray());
} catch (IOException e) { } catch (IOException e) {
@ -146,18 +147,18 @@ public class BitmessageAddress implements Serializable {
this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20); this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20);
// test checksum // test checksum
byte[] checksum = security().doubleSha512(bytes, bytes.length - 4); byte[] checksum = cryptography().doubleSha512(bytes, bytes.length - 4);
byte[] expectedChecksum = bytes(in, 4); byte[] expectedChecksum = bytes(in, 4);
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (expectedChecksum[i] != checksum[i]) if (expectedChecksum[i] != checksum[i])
throw new IllegalArgumentException("Checksum of address failed"); throw new IllegalArgumentException("Checksum of address failed");
} }
if (version < 4) { if (version < 4) {
checksum = security().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); checksum = cryptography().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
this.tag = null; this.tag = null;
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} else { } else {
checksum = security().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); checksum = cryptography().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
this.tag = Arrays.copyOfRange(checksum, 32, 64); this.tag = Arrays.copyOfRange(checksum, 32, 64);
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} }
@ -172,7 +173,7 @@ public class BitmessageAddress implements Serializable {
Encode.varInt(version, out); Encode.varInt(version, out);
Encode.varInt(stream, out); Encode.varInt(stream, out);
out.write(ripe); out.write(ripe);
return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64); return Arrays.copyOfRange(cryptography().doubleSha512(out.toByteArray()), 32, 64);
} catch (IOException e) { } catch (IOException e) {
throw new ApplicationException(e); throw new ApplicationException(e);
} }
@ -266,4 +267,11 @@ public class BitmessageAddress implements Serializable {
public void setChan(boolean chan) { public void setChan(boolean chan) {
this.chan = chan; this.chan = chan;
} }
public boolean has(Feature feature) {
if (pubkey == null || feature == null) {
return false;
}
return feature.isActive(pubkey.getBehaviorBitfield());
}
} }

View File

@ -27,7 +27,7 @@ import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* A network message is exchanged between two nodes. * A network message is exchanged between two nodes.
@ -51,7 +51,7 @@ public class NetworkMessage implements Streamable {
* First 4 bytes of sha512(payload) * First 4 bytes of sha512(payload)
*/ */
private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException { private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException {
byte[] d = security().sha512(bytes); byte[] d = cryptography().sha512(bytes);
return new byte[]{d[0], d[1], d[2], d[3]}; return new byte[]{d[0], d[1], d[2], d[3]};
} }

View File

@ -29,8 +29,10 @@ import ch.dissem.bitmessage.utils.Encode;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays;
import java.util.Objects;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* The 'object' command sends an object that is shared throughout the network. * The 'object' command sends an object that is shared throughout the network.
@ -55,7 +57,7 @@ public class ObjectMessage implements MessagePayload {
expiresTime = builder.expiresTime; expiresTime = builder.expiresTime;
objectType = builder.objectType; objectType = builder.objectType;
version = builder.payload.getVersion(); version = builder.payload.getVersion();
stream = builder.streamNumber; stream = builder.streamNumber > 0 ? builder.streamNumber : builder.payload.getStream();
payload = builder.payload; payload = builder.payload;
} }
@ -94,7 +96,7 @@ public class ObjectMessage implements MessagePayload {
public InventoryVector getInventoryVector() { public InventoryVector getInventoryVector() {
return new InventoryVector( return new InventoryVector(
Bytes.truncate(security().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32) Bytes.truncate(cryptography().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32)
); );
} }
@ -119,7 +121,7 @@ public class ObjectMessage implements MessagePayload {
public void sign(PrivateKey key) { public void sign(PrivateKey key) {
if (payload.isSigned()) { if (payload.isSigned()) {
payload.setSignature(security().getSignature(getBytesToSign(), key)); payload.setSignature(cryptography().getSignature(getBytesToSign(), key));
} }
} }
@ -153,7 +155,7 @@ public class ObjectMessage implements MessagePayload {
public boolean isSignatureValid(Pubkey pubkey) throws IOException { public boolean isSignatureValid(Pubkey pubkey) throws IOException {
if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first"); if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first");
return security().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); return cryptography().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey);
} }
@Override @Override
@ -230,4 +232,29 @@ public class ObjectMessage implements MessagePayload {
return new ObjectMessage(this); return new ObjectMessage(this);
} }
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ObjectMessage that = (ObjectMessage) o;
return expiresTime == that.expiresTime &&
objectType == that.objectType &&
version == that.version &&
stream == that.stream &&
Objects.equals(payload, that.payload);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(nonce);
result = 31 * result + (int) (expiresTime ^ (expiresTime >>> 32));
result = 31 * result + (int) (objectType ^ (objectType >>> 32));
result = 31 * result + (int) (version ^ (version >>> 32));
result = 31 * result + (int) (stream ^ (stream >>> 32));
result = 31 * result + (payload != null ? payload.hashCode() : 0);
return result;
}
} }

View File

@ -16,16 +16,19 @@
package ch.dissem.bitmessage.entity; package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.*;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.UnixTime;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import java.util.Collections;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* The unencrypted message to be sent by 'msg' or 'broadcast'. * The unencrypted message to be sent by 'msg' or 'broadcast'.
@ -37,7 +40,8 @@ public class Plaintext implements Streamable {
private final BitmessageAddress from; private final BitmessageAddress from;
private final long encoding; private final long encoding;
private final byte[] message; private final byte[] message;
private final byte[] ack; private final byte[] ackData;
private ObjectMessage ackMessage;
private Object id; private Object id;
private InventoryVector inventoryVector; private InventoryVector inventoryVector;
private BitmessageAddress to; private BitmessageAddress to;
@ -49,6 +53,10 @@ public class Plaintext implements Streamable {
private Set<Label> labels; private Set<Label> labels;
private byte[] initialHash; private byte[] initialHash;
private long ttl;
private int retries;
private Long nextTry;
private Plaintext(Builder builder) { private Plaintext(Builder builder) {
id = builder.id; id = builder.id;
inventoryVector = builder.inventoryVector; inventoryVector = builder.inventoryVector;
@ -57,12 +65,21 @@ public class Plaintext implements Streamable {
to = builder.to; to = builder.to;
encoding = builder.encoding; encoding = builder.encoding;
message = builder.message; message = builder.message;
ack = builder.ack; ackData = builder.ackData;
if (builder.ackMessage != null && builder.ackMessage.length > 0) {
ackMessage = Factory.getObjectMessage(
3,
new ByteArrayInputStream(builder.ackMessage),
builder.ackMessage.length);
}
signature = builder.signature; signature = builder.signature;
status = builder.status; status = builder.status;
sent = builder.sent; sent = builder.sent;
received = builder.received; received = builder.received;
labels = builder.labels; labels = builder.labels;
ttl = builder.ttl;
retries = builder.retries;
nextTry = builder.nextTry;
} }
public static Plaintext read(Type type, InputStream in) throws IOException { public static Plaintext read(Type type, InputStream in) throws IOException {
@ -85,7 +102,7 @@ public class Plaintext implements Streamable {
.destinationRipe(type == Type.MSG ? Decode.bytes(in, 20) : null) .destinationRipe(type == Type.MSG ? Decode.bytes(in, 20) : null)
.encoding(Decode.varInt(in)) .encoding(Decode.varInt(in))
.message(Decode.varBytes(in)) .message(Decode.varBytes(in))
.ack(type == Type.MSG ? Decode.varBytes(in) : null); .ackMessage(type == Type.MSG ? Decode.varBytes(in) : null);
} }
public InventoryVector getInventoryVector() { public InventoryVector getInventoryVector() {
@ -163,8 +180,13 @@ public class Plaintext implements Streamable {
Encode.varInt(message.length, out); Encode.varInt(message.length, out);
out.write(message); out.write(message);
if (type == Type.MSG) { if (type == Type.MSG) {
Encode.varInt(ack.length, out); if (to.has(Feature.DOES_ACK) && getAckMessage() != null) {
out.write(ack); ByteArrayOutputStream ack = new ByteArrayOutputStream();
getAckMessage().write(ack);
Encode.varBytes(ack.toByteArray(), out);
} else {
Encode.varInt(0, out);
}
} }
if (includeSignature) { if (includeSignature) {
if (signature == null) { if (signature == null) {
@ -206,6 +228,30 @@ public class Plaintext implements Streamable {
this.status = status; this.status = status;
} }
public long getTTL() {
return ttl;
}
public int getRetries() {
return retries;
}
public Long getNextTry() {
return nextTry;
}
public void updateNextTry() {
if (nextTry == null) {
if (sent != null && to.has(Feature.DOES_ACK)) {
nextTry = UnixTime.now(+ttl);
retries++;
}
} else {
nextTry = nextTry + (1 << retries) * ttl;
retries++;
}
}
public String getSubject() { public String getSubject() {
Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8");
String firstLine = s.nextLine(); String firstLine = s.nextLine();
@ -238,7 +284,7 @@ public class Plaintext implements Streamable {
return Objects.equals(encoding, plaintext.encoding) && return Objects.equals(encoding, plaintext.encoding) &&
Objects.equals(from, plaintext.from) && Objects.equals(from, plaintext.from) &&
Arrays.equals(message, plaintext.message) && Arrays.equals(message, plaintext.message) &&
Arrays.equals(ack, plaintext.ack) && Objects.equals(getAckMessage(), plaintext.getAckMessage()) &&
Arrays.equals(to.getRipe(), plaintext.to.getRipe()) && Arrays.equals(to.getRipe(), plaintext.to.getRipe()) &&
Arrays.equals(signature, plaintext.signature) && Arrays.equals(signature, plaintext.signature) &&
Objects.equals(status, plaintext.status) && Objects.equals(status, plaintext.status) &&
@ -249,7 +295,7 @@ public class Plaintext implements Streamable {
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(from, encoding, message, ack, to, signature, status, sent, received, labels); return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels);
} }
public void addLabels(Label... labels) { public void addLabels(Label... labels) {
@ -260,9 +306,32 @@ public class Plaintext implements Streamable {
public void addLabels(Collection<Label> labels) { public void addLabels(Collection<Label> labels) {
if (labels != null) { if (labels != null) {
this.labels.addAll(labels); for (Label label : labels) {
this.labels.add(label);
} }
} }
}
public void removeLabel(Label.Type type) {
Iterator<Label> iterator = labels.iterator();
while (iterator.hasNext()) {
Label label = iterator.next();
if (label.getType() == type) {
iterator.remove();
}
}
}
public byte[] getAckData() {
return ackData;
}
public ObjectMessage getAckMessage() {
if (ackMessage == null) {
ackMessage = Factory.createAck(this);
}
return ackMessage;
}
public void setInitialHash(byte[] initialHash) { public void setInitialHash(byte[] initialHash) {
this.initialHash = initialHash; this.initialHash = initialHash;
@ -316,12 +385,16 @@ public class Plaintext implements Streamable {
private byte[] destinationRipe; private byte[] destinationRipe;
private long encoding; private long encoding;
private byte[] message = new byte[0]; private byte[] message = new byte[0];
private byte[] ack = new byte[0]; private byte[] ackData;
private byte[] ackMessage;
private byte[] signature; private byte[] signature;
private long sent; private long sent;
private long received; private long received;
private Status status; private Status status;
private Set<Label> labels = new HashSet<>(); private Set<Label> labels = new HashSet<>();
private long ttl;
private int retries;
private Long nextTry;
public Builder(Type type) { public Builder(Type type) {
this.type = type; this.type = type;
@ -415,9 +488,16 @@ public class Plaintext implements Streamable {
return this; return this;
} }
public Builder ack(byte[] ack) { public Builder ackMessage(byte[] ack) {
if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ack only allowed for msg"); if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ackMessage only allowed for msg");
this.ack = ack; this.ackMessage = ack;
return this;
}
public Builder ackData(byte[] ackData) {
if (type != Type.MSG && ackData != null)
throw new IllegalArgumentException("ackMessage only allowed for msg");
this.ackData = ackData;
return this; return this;
} }
@ -446,6 +526,21 @@ public class Plaintext implements Streamable {
return this; return this;
} }
public Builder ttl(long ttl) {
this.ttl = ttl;
return this;
}
public Builder retries(int retries) {
this.retries = retries;
return this;
}
public Builder nextTry(Long nextTry) {
this.nextTry = nextTry;
return this;
}
public Plaintext build() { public Plaintext build() {
if (from == null) { if (from == null) {
from = new BitmessageAddress(Factory.createPubkey( from = new BitmessageAddress(Factory.createPubkey(
@ -461,6 +556,12 @@ public class Plaintext implements Streamable {
if (to == null && type != Type.BROADCAST && destinationRipe != null) { if (to == null && type != Type.BROADCAST && destinationRipe != null) {
to = new BitmessageAddress(0, 0, destinationRipe); to = new BitmessageAddress(0, 0, destinationRipe);
} }
if (type == Type.MSG && ackMessage == null && ackData == null) {
ackData = cryptography().randomBytes(Msg.ACK_LENGTH);
}
if (ttl <= 0) {
ttl = TTL.msg();
}
return new Plaintext(this); return new Plaintext(this);
} }
} }

View File

@ -145,13 +145,13 @@ public class Version implements MessagePayload {
private String userAgent; private String userAgent;
private long[] streamNumbers; private long[] streamNumbers;
public Builder defaults() { public Builder defaults(long clientNonce) {
version = BitmessageContext.CURRENT_VERSION; version = BitmessageContext.CURRENT_VERSION;
services = 1; services = 1;
timestamp = UnixTime.now(); timestamp = UnixTime.now();
nonce = new Random().nextInt();
userAgent = "/Jabit:0.0.1/"; userAgent = "/Jabit:0.0.1/";
streamNumbers = new long[]{1}; streamNumbers = new long[]{1};
nonce = clientNonce;
return this; return this;
} }

View File

@ -23,9 +23,10 @@ import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* Users who are subscribed to the sending address will see the message appear in their inbox. * Users who are subscribed to the sending address will see the message appear in their inbox.
@ -80,7 +81,7 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai
} }
public void encrypt() throws IOException { public void encrypt() throws IOException {
encrypt(security().createPublicKey(plaintext.getFrom().getPublicDecryptionKey())); encrypt(cryptography().createPublicKey(plaintext.getFrom().getPublicDecryptionKey()));
} }
@Override @Override
@ -96,4 +97,18 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai
public boolean isDecrypted() { public boolean isDecrypted() {
return plaintext != null; return plaintext != null;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Broadcast broadcast = (Broadcast) o;
return stream == broadcast.stream &&
(Objects.equals(encrypted, broadcast.encrypted) || Objects.equals(plaintext, broadcast.plaintext));
}
@Override
public int hashCode() {
return Objects.hash(stream);
}
} }

View File

@ -27,7 +27,7 @@ import java.io.*;
import java.util.Arrays; import java.util.Arrays;
import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
public class CryptoBox implements Streamable { public class CryptoBox implements Streamable {
@ -50,22 +50,22 @@ public class CryptoBox implements Streamable {
// 1. The destination public key is called K. // 1. The destination public key is called K.
// 2. Generate 16 random bytes using a secure random number generator. Call them IV. // 2. Generate 16 random bytes using a secure random number generator. Call them IV.
initializationVector = security().randomBytes(16); initializationVector = cryptography().randomBytes(16);
// 3. Generate a new random EC key pair with private key called r and public key called R. // 3. Generate a new random EC key pair with private key called r and public key called R.
byte[] r = security().randomBytes(PRIVATE_KEY_SIZE); byte[] r = cryptography().randomBytes(PRIVATE_KEY_SIZE);
R = security().createPublicKey(r); R = cryptography().createPublicKey(r);
// 4. Do an EC point multiply with public key K and private key r. This gives you public key P. // 4. Do an EC point multiply with public key K and private key r. This gives you public key P.
byte[] P = security().multiply(K, r); byte[] P = cryptography().multiply(K, r);
byte[] X = Points.getX(P); byte[] X = Points.getX(P);
// 5. Use the X component of public key P and calculate the SHA512 hash H. // 5. Use the X component of public key P and calculate the SHA512 hash H.
byte[] H = security().sha512(X); byte[] H = cryptography().sha512(X);
// 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. // 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
byte[] key_e = Arrays.copyOfRange(H, 0, 32); byte[] key_e = Arrays.copyOfRange(H, 0, 32);
byte[] key_m = Arrays.copyOfRange(H, 32, 64); byte[] key_m = Arrays.copyOfRange(H, 32, 64);
// 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7. // 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. // 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, data, key_e, initializationVector); encrypted = cryptography().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. // 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); mac = calculateMac(key_m);
@ -75,7 +75,7 @@ public class CryptoBox implements Streamable {
private CryptoBox(Builder builder) { private CryptoBox(Builder builder) {
initializationVector = builder.initializationVector; initializationVector = builder.initializationVector;
curveType = builder.curveType; curveType = builder.curveType;
R = security().createPoint(builder.xComponent, builder.yComponent); R = cryptography().createPoint(builder.xComponent, builder.yComponent);
encrypted = builder.encrypted; encrypted = builder.encrypted;
mac = builder.mac; mac = builder.mac;
} }
@ -101,9 +101,9 @@ public class CryptoBox implements Streamable {
public InputStream decrypt(byte[] k) throws DecryptionFailedException { public InputStream decrypt(byte[] k) throws DecryptionFailedException {
// 1. The private key used to decrypt is called k. // 1. The private key used to decrypt is called k.
// 2. Do an EC point multiply with private key k and public key R. This gives you public key P. // 2. Do an EC point multiply with private key k and public key R. This gives you public key P.
byte[] P = security().multiply(R, k); byte[] P = cryptography().multiply(R, k);
// 3. Use the X component of public key P and calculate the SHA512 hash H. // 3. Use the X component of public key P and calculate the SHA512 hash H.
byte[] H = security().sha512(Arrays.copyOfRange(P, 1, 33)); byte[] H = cryptography().sha512(Arrays.copyOfRange(P, 1, 33));
// 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. // 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
byte[] key_e = Arrays.copyOfRange(H, 0, 32); byte[] key_e = Arrays.copyOfRange(H, 0, 32);
byte[] key_m = Arrays.copyOfRange(H, 32, 64); byte[] key_m = Arrays.copyOfRange(H, 32, 64);
@ -116,14 +116,14 @@ public class CryptoBox implements Streamable {
// 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key // 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key
// and the cipher text as payload. The output is the padded input text. // and the cipher text as payload. The output is the padded input text.
return new ByteArrayInputStream(security().crypt(false, encrypted, key_e, initializationVector)); return new ByteArrayInputStream(cryptography().crypt(false, encrypted, key_e, initializationVector));
} }
private byte[] calculateMac(byte[] key_m) { private byte[] calculateMac(byte[] key_m) {
try { try {
ByteArrayOutputStream macData = new ByteArrayOutputStream(); ByteArrayOutputStream macData = new ByteArrayOutputStream();
writeWithoutMAC(macData); writeWithoutMAC(macData);
return security().mac(key_m, macData.toByteArray()); return cryptography().mac(key_m, macData.toByteArray());
} catch (IOException e) { } catch (IOException e) {
throw new ApplicationException(e); throw new ApplicationException(e);
} }

View File

@ -39,7 +39,7 @@ public class GenericPayload extends ObjectPayload {
this.data = data; this.data = data;
} }
public static GenericPayload read(long version, InputStream is, long stream, int length) throws IOException { public static GenericPayload read(long version, long stream, InputStream is, int length) throws IOException {
return new GenericPayload(version, stream, Decode.bytes(is, length)); return new GenericPayload(version, stream, Decode.bytes(is, length));
} }
@ -53,6 +53,10 @@ public class GenericPayload extends ObjectPayload {
return stream; return stream;
} }
public byte[] getData() {
return data;
}
@Override @Override
public void write(OutputStream stream) throws IOException { public void write(OutputStream stream) throws IOException {
stream.write(data); stream.write(data);

View File

@ -24,6 +24,7 @@ import ch.dissem.bitmessage.exception.DecryptionFailedException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Objects;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
@ -32,6 +33,7 @@ import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
*/ */
public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder { public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder {
private static final long serialVersionUID = 4327495048296365733L; private static final long serialVersionUID = 4327495048296365733L;
public static final int ACK_LENGTH = 32;
private long stream; private long stream;
private CryptoBox encrypted; private CryptoBox encrypted;
@ -108,4 +110,19 @@ public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder {
if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it."); if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it.");
encrypted.write(out); encrypted.write(out);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Msg msg = (Msg) o;
return stream == msg.stream &&
(Objects.equals(encrypted, msg.encrypted) || Objects.equals(plaintext, msg.plaintext));
}
@Override
public int hashCode() {
return (int) stream;
}
} }

View File

@ -20,7 +20,7 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* Public keys for signing and encryption, the answer to a 'getpubkey' request. * Public keys for signing and encryption, the answer to a 'getpubkey' request.
@ -35,7 +35,7 @@ public abstract class Pubkey extends ObjectPayload {
} }
public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) { public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) {
return security().ripemd160(security().sha512(publicSigningKey, publicEncryptionKey)); return cryptography().ripemd160(cryptography().sha512(publicSigningKey, publicEncryptionKey));
} }
public abstract byte[] getSigningKey(); public abstract byte[] getSigningKey();
@ -45,7 +45,7 @@ public abstract class Pubkey extends ObjectPayload {
public abstract int getBehaviorBitfield(); public abstract int getBehaviorBitfield();
public byte[] getRipe() { public byte[] getRipe() {
return security().ripemd160(security().sha512(getSigningKey(), getEncryptionKey())); return cryptography().ripemd160(cryptography().sha512(getSigningKey(), getEncryptionKey()));
} }
public long getNonceTrialsPerByte() { public long getNonceTrialsPerByte() {
@ -76,16 +76,19 @@ public abstract class Pubkey extends ObjectPayload {
* Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg * Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg
* messages bound for them. * messages bound for them.
*/ */
INCLUDE_DESTINATION(1 << 30), INCLUDE_DESTINATION(30),
/** /**
* If true, the receiving node does send acknowledgements (rather than dropping them). * If true, the receiving node does send acknowledgements (rather than dropping them).
*/ */
DOES_ACK(1 << 31); DOES_ACK(31);
private int bit; private int bit;
Feature(int bit) { Feature(int bitNumber) {
this.bit = bit; // The Bitmessage Protocol Specification starts counting at the most significant bit,
// thus the slightly awkward calculation.
// https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features
this.bit = 1 << (31 - bitNumber);
} }
public static int bitfield(Feature... features) { public static int bitfield(Feature... features) {
@ -105,5 +108,9 @@ public abstract class Pubkey extends ObjectPayload {
} }
return features.toArray(new Feature[features.size()]); return features.toArray(new Feature[features.size()]);
} }
public boolean isActive(int bitfield) {
return (bitfield & bit) != 0;
}
} }
} }

View File

@ -30,7 +30,7 @@ import java.io.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying * Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying
@ -53,15 +53,15 @@ public class PrivateKey implements Streamable {
byte[] pubEK; byte[] pubEK;
byte[] ripe; byte[] ripe;
do { do {
privSK = security().randomBytes(PRIVATE_KEY_SIZE); privSK = cryptography().randomBytes(PRIVATE_KEY_SIZE);
privEK = security().randomBytes(PRIVATE_KEY_SIZE); privEK = cryptography().randomBytes(PRIVATE_KEY_SIZE);
pubSK = security().createPublicKey(privSK); pubSK = cryptography().createPublicKey(privSK);
pubEK = security().createPublicKey(privEK); pubEK = cryptography().createPublicKey(privEK);
ripe = Pubkey.getRipe(pubSK, pubEK); ripe = Pubkey.getRipe(pubSK, pubEK);
} while (ripe[0] != 0 || (shorter && ripe[1] != 0)); } while (ripe[0] != 0 || (shorter && ripe[1] != 0));
this.privateSigningKey = privSK; this.privateSigningKey = privSK;
this.privateEncryptionKey = privEK; this.privateEncryptionKey = privEK;
this.pubkey = security().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, this.pubkey = cryptography().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey,
nonceTrialsPerByte, extraBytes, features); nonceTrialsPerByte, extraBytes, features);
} }
@ -118,11 +118,11 @@ public class PrivateKey implements Streamable {
long encryptionKeyNonce = nextNonce + 1; long encryptionKeyNonce = nextNonce + 1;
byte[] ripe; byte[] ripe;
do { do {
privEK = Bytes.truncate(security().sha512(seed, Encode.varInt(encryptionKeyNonce)), 32); privEK = Bytes.truncate(cryptography().sha512(seed, Encode.varInt(encryptionKeyNonce)), 32);
privSK = Bytes.truncate(security().sha512(seed, Encode.varInt(signingKeyNonce)), 32); privSK = Bytes.truncate(cryptography().sha512(seed, Encode.varInt(signingKeyNonce)), 32);
pubSK = security().createPublicKey(privSK); pubSK = cryptography().createPublicKey(privSK);
pubEK = security().createPublicKey(privEK); pubEK = cryptography().createPublicKey(privEK);
ripe = security().ripemd160(security().sha512(pubSK, pubEK)); ripe = cryptography().ripemd160(cryptography().sha512(pubSK, pubEK));
signingKeyNonce += 2; signingKeyNonce += 2;
encryptionKeyNonce += 2; encryptionKeyNonce += 2;

View File

@ -23,6 +23,8 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.exception.NodeException;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -31,7 +33,8 @@ import java.io.InputStream;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.entity.payload.ObjectType.MSG;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* Creates {@link NetworkMessage} objects from {@link InputStream InputStreams} * Creates {@link NetworkMessage} objects from {@link InputStream InputStreams}
@ -116,8 +119,8 @@ public class Factory {
BitmessageAddress temp = new BitmessageAddress(address); BitmessageAddress temp = new BitmessageAddress(address);
PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey, PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey,
createPubkey(temp.getVersion(), temp.getStream(), createPubkey(temp.getVersion(), temp.getStream(),
security().createPublicKey(privateSigningKey), cryptography().createPublicKey(privateSigningKey),
security().createPublicKey(privateEncryptionKey), cryptography().createPublicKey(privateEncryptionKey),
nonceTrialsPerByte, extraBytes, behaviourBitfield)); nonceTrialsPerByte, extraBytes, behaviourBitfield));
BitmessageAddress result = new BitmessageAddress(privateKey); BitmessageAddress result = new BitmessageAddress(privateKey);
if (!result.getAddress().equals(address)) { if (!result.getAddress().equals(address)) {
@ -155,7 +158,7 @@ public class Factory {
} }
// fallback: just store the message - we don't really care what it is // fallback: just store the message - we don't really care what it is
LOG.trace("Unexpected object type: " + objectType); LOG.trace("Unexpected object type: " + objectType);
return GenericPayload.read(version, stream, streamNumber, length); return GenericPayload.read(version, streamNumber, stream, length);
} }
private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException {
@ -177,7 +180,7 @@ public class Factory {
private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException {
Pubkey pubkey = readPubkey(version, streamNumber, stream, length, true); Pubkey pubkey = readPubkey(version, streamNumber, stream, length, true);
return pubkey != null ? pubkey : GenericPayload.read(version, stream, streamNumber, length); return pubkey != null ? pubkey : GenericPayload.read(version, streamNumber, stream, length);
} }
private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException { private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException {
@ -192,7 +195,7 @@ public class Factory {
return V5Broadcast.read(stream, streamNumber, length); return V5Broadcast.read(stream, streamNumber, length);
default: default:
LOG.debug("Encountered unknown broadcast version " + version); LOG.debug("Encountered unknown broadcast version " + version);
return GenericPayload.read(version, stream, streamNumber, length); return GenericPayload.read(version, streamNumber, stream, length);
} }
} }
@ -204,4 +207,11 @@ public class Factory {
return new V5Broadcast(sendingAddress, plaintext); return new V5Broadcast(sendingAddress, plaintext);
} }
} }
public static ObjectMessage createAck(Plaintext plaintext) {
if (plaintext == null || plaintext.getAckData() == null)
return null;
GenericPayload ack = new GenericPayload(3, plaintext.getFrom().getStream(), plaintext.getAckData());
return new ObjectMessage.Builder().objectType(MSG).payload(ack).expiresTime(UnixTime.now(plaintext.getTTL())).build();
}
} }

View File

@ -32,7 +32,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* Creates protocol v3 network messages from {@link InputStream InputStreams} * Creates protocol v3 network messages from {@link InputStream InputStreams}
@ -183,7 +183,7 @@ class V3MessageFactory {
} }
private static boolean testChecksum(byte[] checksum, byte[] payload) { private static boolean testChecksum(byte[] checksum, byte[] payload) {
byte[] payloadChecksum = security().sha512(payload); byte[] payloadChecksum = cryptography().sha512(payload);
for (int i = 0; i < checksum.length; i++) { for (int i = 0; i < checksum.length; i++) {
if (checksum[i] != payloadChecksum[i]) { if (checksum[i] != payloadChecksum[i]) {
return false; return false;

View File

@ -0,0 +1,123 @@
/*
* Copyright 2016 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.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Strings;
import ch.dissem.bitmessage.utils.UnixTime;
import java.util.Collection;
import java.util.List;
import static ch.dissem.bitmessage.utils.SqlStrings.join;
public abstract class AbstractMessageRepository implements MessageRepository, InternalContext.ContextHolder {
protected InternalContext ctx;
@Override
public void setContext(InternalContext context) {
this.ctx = context;
}
protected void safeSenderIfNecessary(Plaintext message) {
if (message.getId() == null) {
BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress());
if (savedAddress == null) {
ctx.getAddressRepository().save(message.getFrom());
} else if (savedAddress.getPubkey() == null && message.getFrom().getPubkey() != null) {
savedAddress.setPubkey(message.getFrom().getPubkey());
ctx.getAddressRepository().save(savedAddress);
}
}
}
@Override
public Plaintext getMessage(Object id) {
if (id instanceof Long) {
return single(find("id=" + id));
} else {
throw new IllegalArgumentException("Long expected for ID");
}
}
@Override
public Plaintext getMessage(byte[] initialHash) {
return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'"));
}
@Override
public Plaintext getMessageForAck(byte[] ackData) {
return single(find("ack_data=X'" + Strings.hex(ackData) + "' AND status='" + Plaintext.Status.SENT + "'"));
}
@Override
public List<Plaintext> findMessages(Label label) {
return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")");
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) {
return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'");
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status) {
return find("status='" + status.name() + "'");
}
@Override
public List<Plaintext> findMessages(BitmessageAddress sender) {
return find("sender='" + sender.getAddress() + "'");
}
@Override
public List<Plaintext> findMessagesToResend() {
return find("status='" + Plaintext.Status.SENT.name() + "'" +
" AND next_try < " + UnixTime.now());
}
@Override
public List<Label> getLabels() {
return findLabels("1=1");
}
@Override
public List<Label> getLabels(Label.Type... types) {
return findLabels("type IN (" + join(types) + ")");
}
protected abstract List<Label> findLabels(String where);
protected <T> T single(Collection<T> collection) {
switch (collection.size()) {
case 0:
return null;
case 1:
return collection.iterator().next();
default:
throw new ApplicationException("This shouldn't happen, found " + collection.size() +
" items, one or none was expected");
}
}
protected abstract List<Plaintext> find(String where);
}

View File

@ -20,13 +20,14 @@ import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import java.util.Iterator; import static ch.dissem.bitmessage.entity.Plaintext.Status.*;
public class DefaultLabeler implements Labeler, InternalContext.ContextHolder { public class DefaultLabeler implements Labeler, InternalContext.ContextHolder {
private InternalContext ctx; private InternalContext ctx;
@Override @Override
public void setLabels(Plaintext msg) { public void setLabels(Plaintext msg) {
msg.setStatus(RECEIVED);
if (msg.getType() == Plaintext.Type.BROADCAST) { if (msg.getType() == Plaintext.Type.BROADCAST) {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)); msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
} else { } else {
@ -34,15 +35,38 @@ public class DefaultLabeler implements Labeler, InternalContext.ContextHolder {
} }
} }
@Override
public void markAsDraft(Plaintext msg) {
msg.setStatus(DRAFT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.DRAFT));
}
@Override
public void markAsSending(Plaintext msg) {
if (msg.getTo() != null && msg.getTo().getPubkey() == null) {
msg.setStatus(PUBKEY_REQUESTED);
} else {
msg.setStatus(DOING_PROOF_OF_WORK);
}
msg.removeLabel(Label.Type.DRAFT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX));
}
@Override
public void markAsSent(Plaintext msg) {
msg.setStatus(SENT);
msg.removeLabel(Label.Type.OUTBOX);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT));
}
@Override
public void markAsAcknowledged(Plaintext msg) {
msg.setStatus(SENT_ACKNOWLEDGED);
}
@Override @Override
public void markAsRead(Plaintext msg) { public void markAsRead(Plaintext msg) {
Iterator<Label> iterator = msg.getLabels().iterator(); msg.removeLabel(Label.Type.UNREAD);
while (iterator.hasNext()) {
Label label = iterator.next();
if (label.getType() == Label.Type.UNREAD) {
iterator.remove();
}
}
} }
@Override @Override

View File

@ -19,7 +19,13 @@ package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
/** /**
* Defines and sets labels * Defines and sets labels. Note that it should also update the status field of a message.
* Generally it's highly advised to override the {@link DefaultLabeler} whenever possible,
* instead of directly implementing the interface.
* <p>
* As the labeler gets called whenever the state of a message changes, it can also be used
* as a listener.
* </p>
*/ */
public interface Labeler { public interface Labeler {
/** /**
@ -29,6 +35,18 @@ public interface Labeler {
*/ */
void setLabels(Plaintext msg); void setLabels(Plaintext msg);
void markAsDraft(Plaintext msg);
/**
* It is paramount that this methods marks the {@link Plaintext} object with status
* {@link Plaintext.Status#PUBKEY_REQUESTED} (see {@link DefaultLabeler})
*/
void markAsSending(Plaintext msg);
void markAsSent(Plaintext msg);
void markAsAcknowledged(Plaintext msg);
void markAsRead(Plaintext msg); void markAsRead(Plaintext msg);
void markAsUnread(Plaintext msg); void markAsUnread(Plaintext msg);

View File

@ -46,15 +46,11 @@ public class MemoryNodeRegistry implements NodeRegistry {
while (scanner.hasNext()) { while (scanner.hasNext()) {
try { try {
String line = scanner.nextLine().trim(); String line = scanner.nextLine().trim();
if (line.startsWith("#") || line.isEmpty()) {
// Ignore
continue;
}
if (line.startsWith("[stream")) { if (line.startsWith("[stream")) {
stream = Long.parseLong(line.substring(8, line.lastIndexOf(']'))); stream = Long.parseLong(line.substring(8, line.lastIndexOf(']')));
streamSet = new HashSet<>(); streamSet = new HashSet<>();
stableNodes.put(stream, streamSet); stableNodes.put(stream, streamSet);
} else if (streamSet != null) { } else if (streamSet != null && !line.isEmpty() && !line.startsWith("#")) {
int portIndex = line.lastIndexOf(':'); int portIndex = line.lastIndexOf(':');
InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex)); InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex));
int port = Integer.valueOf(line.substring(portIndex + 1)); int port = Integer.valueOf(line.substring(portIndex + 1));
@ -89,12 +85,12 @@ public class MemoryNodeRegistry implements NodeRegistry {
known.remove(node); known.remove(node);
} }
} }
} else {
Set<NetworkAddress> nodes = stableNodes.get(stream);
if (nodes == null || nodes.isEmpty()) {
loadStableNodes();
nodes = stableNodes.get(stream);
} }
if (result.isEmpty()) {
if (stableNodes.isEmpty()) {
loadStableNodes();
}
Set<NetworkAddress> nodes = stableNodes.get(stream);
if (nodes != null && !nodes.isEmpty()) { if (nodes != null && !nodes.isEmpty()) {
// To reduce load on stable nodes, only return one // To reduce load on stable nodes, only return one
result.add(selectRandom(nodes)); result.add(selectRandom(nodes));

View File

@ -30,8 +30,12 @@ public interface MessageRepository {
int countUnread(Label label); int countUnread(Label label);
Plaintext getMessage(Object id);
Plaintext getMessage(byte[] initialHash); Plaintext getMessage(byte[] initialHash);
Plaintext getMessageForAck(byte[] ackData);
List<Plaintext> findMessages(Label label); List<Plaintext> findMessages(Label label);
List<Plaintext> findMessages(Status status); List<Plaintext> findMessages(Status status);
@ -40,6 +44,8 @@ public interface MessageRepository {
List<Plaintext> findMessages(BitmessageAddress sender); List<Plaintext> findMessages(BitmessageAddress sender);
List<Plaintext> findMessagesToResend();
void save(Plaintext message); void save(Plaintext message);
void remove(Plaintext message); void remove(Plaintext message);

View File

@ -1,6 +1,7 @@
package ch.dissem.bitmessage.ports; package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import java.util.List; import java.util.List;
@ -16,6 +17,8 @@ public interface ProofOfWorkRepository {
void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes); void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
void putObject(Item item);
void removeObject(byte[] initialHash); void removeObject(byte[] initialHash);
class Item { class Item {
@ -23,10 +26,20 @@ public interface ProofOfWorkRepository {
public final long nonceTrialsPerByte; public final long nonceTrialsPerByte;
public final long extraBytes; public final long extraBytes;
// Needed for ACK POW calculation
public final Long expirationTime;
public final Plaintext message;
public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
this(object, nonceTrialsPerByte, extraBytes, 0, null);
}
public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, long expirationTime, Plaintext message) {
this.object = object; this.object = object;
this.nonceTrialsPerByte = nonceTrialsPerByte; this.nonceTrialsPerByte = nonceTrialsPerByte;
this.extraBytes = extraBytes; this.extraBytes = extraBytes;
this.expirationTime = expirationTime;
this.message = message;
} }
} }
} }

View File

@ -30,7 +30,7 @@ public class Singleton {
} }
} }
public static Cryptography security() { public static Cryptography cryptography() {
return cryptography; return cryptography;
} }
} }

View File

@ -0,0 +1,59 @@
/*
* Copyright 2016 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 ch.dissem.bitmessage.entity.payload.ObjectType;
import static ch.dissem.bitmessage.utils.Strings.hex;
public class SqlStrings {
public static StringBuilder join(long... objects) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(objects[i]);
}
return streamList;
}
public static StringBuilder join(byte[]... objects) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(hex(objects[i]));
}
return streamList;
}
public static StringBuilder join(ObjectType... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(types[i].getNumber());
}
return streamList;
}
public static StringBuilder join(Enum... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append('\'').append(types[i].name()).append('\'');
}
return streamList;
}
}

View File

@ -20,11 +20,14 @@ import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.Plaintext.Type;
import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.MessageMatchers; import ch.dissem.bitmessage.utils.MessageMatchers;
import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.TestUtils; import ch.dissem.bitmessage.utils.TestUtils;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
@ -35,6 +38,8 @@ import java.util.*;
import static ch.dissem.bitmessage.entity.payload.ObjectType.*; import static ch.dissem.bitmessage.entity.payload.ObjectType.*;
import static ch.dissem.bitmessage.utils.MessageMatchers.object; import static ch.dissem.bitmessage.utils.MessageMatchers.object;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -56,13 +61,50 @@ public class BitmessageContextTest {
.cryptography(new BouncyCryptography()) .cryptography(new BouncyCryptography())
.inventory(mock(Inventory.class)) .inventory(mock(Inventory.class))
.listener(listener) .listener(listener)
.messageCallback(mock(MessageCallback.class))
.messageRepo(mock(MessageRepository.class)) .messageRepo(mock(MessageRepository.class))
.networkHandler(mock(NetworkHandler.class)) .networkHandler(mock(NetworkHandler.class))
.nodeRegistry(mock(NodeRegistry.class)) .nodeRegistry(mock(NodeRegistry.class))
.powRepo(mock(ProofOfWorkRepository.class)) .labeler(spy(new DefaultLabeler()))
.proofOfWorkEngine(mock(ProofOfWorkEngine.class)) .powRepo(spy(new ProofOfWorkRepository() {
Map<InventoryVector, Item> items = new HashMap<>();
@Override
public Item getItem(byte[] initialHash) {
return items.get(new InventoryVector(initialHash));
}
@Override
public List<byte[]> getItems() {
List<byte[]> result = new LinkedList<>();
for (InventoryVector iv : items.keySet()) {
result.add(iv.getHash());
}
return result;
}
@Override
public void putObject(Item item) {
items.put(new InventoryVector(cryptography().getInitialHash(item.object)), item);
}
@Override
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
items.put(new InventoryVector(cryptography().getInitialHash(object)), new Item(object, nonceTrialsPerByte, extraBytes));
}
@Override
public void removeObject(byte[] initialHash) {
items.remove(initialHash);
}
}))
.proofOfWorkEngine(spy(new ProofOfWorkEngine() {
@Override
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
callback.onNonceCalculated(initialHash, new byte[8]);
}
}))
.build(); .build();
TTL.msg(2 * MINUTE);
} }
@Test @Test
@ -162,9 +204,10 @@ public class BitmessageContextTest {
public void ensureMessageIsSent() throws Exception { public void ensureMessageIsSent() throws Exception {
ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(), ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(),
"Subject", "Message"); "Subject", "Message");
assertEquals(2, ctx.internals().getProofOfWorkRepository().getItems().size());
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(MSG), eq(1000L), eq(1000L)); .putObject(object(MSG), eq(1000L), eq(1000L));
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Plaintext.Type.MSG)); verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG));
} }
@Test @Test
@ -174,7 +217,7 @@ public class BitmessageContextTest {
"Subject", "Message"); "Subject", "Message");
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(GET_PUBKEY), eq(1000L), eq(1000L)); .putObject(object(GET_PUBKEY), eq(1000L), eq(1000L));
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Plaintext.Type.MSG)); verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@ -193,12 +236,12 @@ public class BitmessageContextTest {
verify(ctx.internals().getProofOfWorkEngine()) verify(ctx.internals().getProofOfWorkEngine())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
verify(ctx.messages(), timeout(10000).atLeastOnce()) verify(ctx.messages(), timeout(10000).atLeastOnce())
.save(MessageMatchers.plaintext(Plaintext.Type.BROADCAST)); .save(MessageMatchers.plaintext(Type.BROADCAST));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void ensureSenderWithoutPrivateKeyThrowsException() { public void ensureSenderWithoutPrivateKeyThrowsException() {
Plaintext msg = new Plaintext.Builder(Plaintext.Type.BROADCAST) Plaintext msg = new Plaintext.Builder(Type.BROADCAST)
.from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.message("Subject", "Message") .message("Subject", "Message")
.build(); .build();
@ -254,4 +297,19 @@ public class BitmessageContextTest {
assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION); assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION);
assertTrue(chan.isChan()); assertTrue(chan.isChan());
} }
@Test
public void ensureUnacknowledgedMessageIsResent() throws Exception {
Plaintext plaintext = new Plaintext.Builder(Type.MSG)
.ttl(1)
.message("subject", "message")
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
.build();
assertTrue(plaintext.getTo().has(Pubkey.Feature.DOES_ACK));
when(ctx.messages().findMessagesToResend()).thenReturn(Collections.singletonList(plaintext));
when(ctx.messages().getMessage(any(byte[].class))).thenReturn(plaintext);
ctx.resendUnacknowledgedMessages();
verify(ctx.labeler(), timeout(1000).times(1)).markAsSent(eq(plaintext));
}
} }

View File

@ -24,9 +24,7 @@ import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.GetPubkey; import ch.dissem.bitmessage.entity.payload.GetPubkey;
import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.ports.Labeler;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils; import ch.dissem.bitmessage.utils.TestUtils;
@ -51,6 +49,10 @@ public class DefaultMessageListenerTest extends TestBase {
private AddressRepository addressRepo; private AddressRepository addressRepo;
@Mock @Mock
private MessageRepository messageRepo; private MessageRepository messageRepo;
@Mock
private Inventory inventory;
@Mock
private NetworkHandler networkHandler;
private InternalContext ctx; private InternalContext ctx;
private DefaultMessageListener listener; private DefaultMessageListener listener;
@ -62,6 +64,9 @@ public class DefaultMessageListenerTest extends TestBase {
Singleton.initialize(new BouncyCryptography()); Singleton.initialize(new BouncyCryptography());
when(ctx.getAddressRepository()).thenReturn(addressRepo); when(ctx.getAddressRepository()).thenReturn(addressRepo);
when(ctx.getMessageRepository()).thenReturn(messageRepo); when(ctx.getMessageRepository()).thenReturn(messageRepo);
when(ctx.getInventory()).thenReturn(inventory);
when(ctx.getNetworkHandler()).thenReturn(networkHandler);
when(ctx.getLabeler()).thenReturn(mock(Labeler.class));
listener = new DefaultMessageListener(ctx, mock(Labeler.class), mock(BitmessageContext.Listener.class)); listener = new DefaultMessageListener(ctx, mock(Labeler.class), mock(BitmessageContext.Listener.class));
} }
@ -94,7 +99,7 @@ public class DefaultMessageListenerTest extends TestBase {
.payload(identity.getPubkey()) .payload(identity.getPubkey())
.build(); .build();
objectMessage.sign(identity.getPrivateKey()); objectMessage.sign(identity.getPrivateKey());
objectMessage.encrypt(Singleton.security().createPublicKey(identity.getPublicDecryptionKey())); objectMessage.encrypt(Singleton.cryptography().createPublicKey(identity.getPublicDecryptionKey()));
listener.receive(objectMessage); listener.receive(objectMessage);
verify(addressRepo).save(any(BitmessageAddress.class)); verify(addressRepo).save(any(BitmessageAddress.class));
@ -104,6 +109,7 @@ public class DefaultMessageListenerTest extends TestBase {
public void ensureIncomingMessageIsSaved() throws Exception { public void ensureIncomingMessageIsSaved() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); BitmessageAddress contact = new BitmessageAddress(identity.getAddress());
contact.setPubkey(identity.getPubkey());
when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity)); when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity));

View File

@ -30,19 +30,19 @@ import org.junit.Test;
import java.io.IOException; import java.io.IOException;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
public class EncryptionTest extends TestBase { public class EncryptionTest extends TestBase {
@Test @Test
public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException { public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException {
GenericPayload before = new GenericPayload(0, 1, security().randomBytes(100)); GenericPayload before = new GenericPayload(0, 1, cryptography().randomBytes(100));
PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000);
CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey()); CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey());
GenericPayload after = GenericPayload.read(0, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 1, 100); GenericPayload after = GenericPayload.read(0, 1, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 100);
assertEquals(before, after); assertEquals(before, after);
} }

View File

@ -65,6 +65,7 @@ public class ProofOfWorkServiceTest {
when(ctx.getInventory()).thenReturn(inventory); when(ctx.getInventory()).thenReturn(inventory);
when(ctx.getNetworkHandler()).thenReturn(networkHandler); when(ctx.getNetworkHandler()).thenReturn(networkHandler);
when(ctx.getMessageRepository()).thenReturn(messageRepo); when(ctx.getMessageRepository()).thenReturn(messageRepo);
when(ctx.getLabeler()).thenReturn(mock(Labeler.class));
proofOfWorkService = new ProofOfWorkService(); proofOfWorkService = new ProofOfWorkService();
proofOfWorkService.setContext(ctx); proofOfWorkService.setContext(ctx);
@ -76,9 +77,9 @@ public class ProofOfWorkServiceTest {
when(proofOfWorkRepo.getItem(any(byte[].class))).thenReturn(new ProofOfWorkRepository.Item(null, 1001, 1002)); when(proofOfWorkRepo.getItem(any(byte[].class))).thenReturn(new ProofOfWorkRepository.Item(null, 1001, 1002));
doNothing().when(cryptography).doProofOfWork(any(ObjectMessage.class), anyLong(), anyLong(), any(ProofOfWorkEngine.Callback.class)); doNothing().when(cryptography).doProofOfWork(any(ObjectMessage.class), anyLong(), anyLong(), any(ProofOfWorkEngine.Callback.class));
proofOfWorkService.doMissingProofOfWork(); proofOfWorkService.doMissingProofOfWork(10);
verify(cryptography).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L), verify(cryptography, timeout(1000)).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L),
any(ProofOfWorkEngine.Callback.class)); any(ProofOfWorkEngine.Callback.class));
} }

View File

@ -20,20 +20,25 @@ import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.Base58; import ch.dissem.bitmessage.utils.*;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Strings;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test; import org.junit.Test;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.INCLUDE_DESTINATION;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class BitmessageAddressTest { public class BitmessageAddressTest extends TestBase {
@Test
public void ensureFeatureFlagIsCalculatedCorrectly() {
assertEquals(1, Pubkey.Feature.bitfield(DOES_ACK));
assertEquals(2, Pubkey.Feature.bitfield(INCLUDE_DESTINATION));
assertEquals(3, Pubkey.Feature.bitfield(DOES_ACK, INCLUDE_DESTINATION));
}
@Test @Test
public void ensureBase58DecodesCorrectly() { public void ensureBase58DecodesCorrectly() {
assertHexEquals("800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D", assertHexEquals("800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D",
@ -61,6 +66,7 @@ public class BitmessageAddressTest {
public void ensureIdentityCanBeCreated() { public void ensureIdentityCanBeCreated() {
BitmessageAddress address = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); BitmessageAddress address = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK));
assertNotNull(address.getPubkey()); assertNotNull(address.getPubkey());
assertTrue(address.has(DOES_ACK));
} }
@Test @Test
@ -90,6 +96,7 @@ public class BitmessageAddressTest {
} }
assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe());
assertTrue(address.has(DOES_ACK));
} }
@Test @Test
@ -104,6 +111,7 @@ public class BitmessageAddressTest {
} catch (Exception e) { } catch (Exception e) {
fail(e.getMessage()); fail(e.getMessage());
} }
assertTrue(address.has(DOES_ACK));
} }
@Test @Test
@ -118,7 +126,7 @@ public class BitmessageAddressTest {
System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n"); System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
security().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))); cryptography().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals(address_string, address.getAddress()); assertEquals(address_string, address.getAddress());
} }
@ -128,7 +136,7 @@ public class BitmessageAddressTest {
byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU");
byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); cryptography().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress());
} }
@ -143,7 +151,7 @@ public class BitmessageAddressTest {
if (bytes.length != 37) if (bytes.length != 37)
throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long");
byte[] hash = security().doubleSha256(bytes, 33); byte[] hash = cryptography().doubleSha256(bytes, 33);
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat); if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat);
} }

View File

@ -30,7 +30,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class SerializationTest extends TestBase { public class SerializationTest extends TestBase {
@ -82,7 +82,7 @@ public class SerializationTest extends TestBase {
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact()) .to(TestUtils.loadContact())
.message("Subject", "Message") .message("Subject", "Message")
.ack("ack".getBytes()) .ackData("ackMessage".getBytes())
.signature(new byte[0]) .signature(new byte[0])
.build(); .build();
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
@ -98,11 +98,37 @@ public class SerializationTest extends TestBase {
assertEquals(p1, p2); assertEquals(p1, p2);
} }
@Test
public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception {
Plaintext p1 = new Plaintext.Builder(MSG)
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
.message("Subject", "Message")
.ackData("ackMessage".getBytes())
.signature(new byte[0])
.build();
ObjectMessage ackMessage1 = p1.getAckMessage();
assertNotNull(ackMessage1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
p1.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
Plaintext p2 = Plaintext.read(MSG, in);
// Received is automatically set on deserialization, so we'll need to set it to 0
Field received = Plaintext.class.getDeclaredField("received");
received.setAccessible(true);
received.set(p2, 0L);
assertEquals(p1, p2);
assertEquals(ackMessage1, p2.getAckMessage());
}
@Test @Test
public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception {
ArrayList<InventoryVector> ivs = new ArrayList<>(50000); ArrayList<InventoryVector> ivs = new ArrayList<>(50000);
for (int i = 0; i < 50000; i++) { for (int i = 0; i < 50000; i++) {
ivs.add(new InventoryVector(security().randomBytes(32))); ivs.add(new InventoryVector(cryptography().randomBytes(32)));
} }
Inv inv = new Inv.Builder().inventory(ivs).build(); Inv inv = new Inv.Builder().inventory(ivs).build();

View File

@ -0,0 +1,99 @@
/*
* Copyright 2016 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.valueobject.NetworkAddress;
import ch.dissem.bitmessage.utils.UnixTime;
import org.junit.Test;
import java.util.Arrays;
import static ch.dissem.bitmessage.utils.UnixTime.HOUR;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
public class NodeRegistryTest {
private NodeRegistry registry = new MemoryNodeRegistry();
@Test
public void ensureGetKnownNodesWithoutStreamsYieldsEmpty() {
assertThat(registry.getKnownAddresses(10), empty());
}
/**
* Please note that this test fails if there is no internet connection,
* as the initial nodes' IP addresses are determined by DNS lookup.
*/
@Test
public void ensureGetKnownNodesForStream1YieldsResult() {
assertThat(registry.getKnownAddresses(10, 1), hasSize(1));
}
@Test
public void ensureNodeIsStored() {
registry.offerAddresses(Arrays.asList(
new NetworkAddress.Builder()
.ipv4(127, 0, 0, 1)
.port(42)
.stream(1)
.time(UnixTime.now())
.build(),
new NetworkAddress.Builder()
.ipv4(127, 0, 0, 2)
.port(42)
.stream(1)
.time(UnixTime.now())
.build(),
new NetworkAddress.Builder()
.ipv4(127, 0, 0, 2)
.port(42)
.stream(2)
.time(UnixTime.now())
.build()
));
assertThat(registry.getKnownAddresses(10, 1).size(), is(2));
assertThat(registry.getKnownAddresses(10, 2).size(), is(1));
assertThat(registry.getKnownAddresses(10, 1, 2).size(), is(3));
}
@Test
public void ensureOldNodesAreRemoved() {
registry.offerAddresses(Arrays.asList(
new NetworkAddress.Builder()
.ipv4(127, 0, 0, 1)
.port(42)
.stream(1)
.time(UnixTime.now())
.build(),
new NetworkAddress.Builder()
.ipv4(127, 0, 0, 2)
.port(42)
.stream(1)
.time(UnixTime.now(-4 * HOUR))
.build(),
new NetworkAddress.Builder()
.ipv4(127, 0, 0, 2)
.port(42)
.stream(2)
.time(UnixTime.now())
.build()
));
assertThat(registry.getKnownAddresses(10, 1).size(), is(1));
assertThat(registry.getKnownAddresses(10, 2).size(), is(1));
assertThat(registry.getKnownAddresses(10, 1, 2).size(), is(2));
}
}

View File

@ -21,7 +21,7 @@ import ch.dissem.bitmessage.utils.CallbackWaiter;
import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestBase;
import org.junit.Test; import org.junit.Test;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class ProofOfWorkEngineTest extends TestBase { public class ProofOfWorkEngineTest extends TestBase {
@ -36,7 +36,7 @@ public class ProofOfWorkEngineTest extends TestBase {
} }
private void testPOW(ProofOfWorkEngine engine) throws InterruptedException { private void testPOW(ProofOfWorkEngine engine) throws InterruptedException {
byte[] initialHash = security().sha512(new byte[]{1, 3, 6, 4}); byte[] initialHash = cryptography().sha512(new byte[]{1, 3, 6, 4});
byte[] target = {0, 0, 0, -1, -1, -1, -1, -1}; byte[] target = {0, 0, 0, -1, -1, -1, -1, -1};
final CallbackWaiter<byte[]> waiter1 = new CallbackWaiter<>(); final CallbackWaiter<byte[]> waiter1 = new CallbackWaiter<>();
@ -49,10 +49,10 @@ public class ProofOfWorkEngineTest extends TestBase {
}); });
byte[] nonce = waiter1.waitForValue(); byte[] nonce = waiter1.waitForValue();
System.out.println("Calculating nonce took " + waiter1.getTime() + "ms"); System.out.println("Calculating nonce took " + waiter1.getTime() + "ms");
assertTrue(Bytes.lt(security().doubleSha512(nonce, initialHash), target, 8)); assertTrue(Bytes.lt(cryptography().doubleSha512(nonce, initialHash), target, 8));
// Let's add a second (shorter) run to find possible multi threading issues // Let's add a second (shorter) run to find possible multi threading issues
byte[] initialHash2 = security().sha512(new byte[]{1, 3, 6, 5}); byte[] initialHash2 = cryptography().sha512(new byte[]{1, 3, 6, 5});
byte[] target2 = {0, 0, -1, -1, -1, -1, -1, -1}; byte[] target2 = {0, 0, -1, -1, -1, -1, -1, -1};
final CallbackWaiter<byte[]> waiter2 = new CallbackWaiter<>(); final CallbackWaiter<byte[]> waiter2 = new CallbackWaiter<>();
@ -65,7 +65,7 @@ public class ProofOfWorkEngineTest extends TestBase {
}); });
byte[] nonce2 = waiter2.waitForValue(); byte[] nonce2 = waiter2.waitForValue();
System.out.println("Calculating nonce took " + waiter2.getTime() + "ms"); System.out.println("Calculating nonce took " + waiter2.getTime() + "ms");
assertTrue(Bytes.lt(security().doubleSha512(nonce2, initialHash2), target2, 8)); assertTrue(Bytes.lt(cryptography().doubleSha512(nonce2, initialHash2), target2, 8));
assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime()); assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime());
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015 Christian Basler * Copyright 2016 Christian Basler
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,16 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.bitmessage.repository; package ch.dissem.bitmessage.utils;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class JdbcHelperTest { public class SqlStringsTest {
@Test @Test
public void ensureJoinWorksWithLongArray() { public void ensureJoinWorksWithLongArray() {
long[] test = {1L, 2L}; long[] test = {1L, 2L};
assertEquals("1, 2", JdbcHelper.join(test).toString()); assertEquals("1, 2", SqlStrings.join(test).toString());
} }
} }

View File

@ -72,7 +72,7 @@ public class TestUtils {
public static BitmessageAddress loadContact() throws IOException, DecryptionFailedException { public static BitmessageAddress loadContact() throws IOException, DecryptionFailedException {
BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h");
ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload"); ObjectMessage object = TestUtils.loadObjectMessage(3, "V4Pubkey.payload");
object.decrypt(address.getPublicDecryptionKey()); object.decrypt(address.getPublicDecryptionKey());
address.setPubkey((V4Pubkey) object.getPayload()); address.setPubkey((V4Pubkey) object.getPayload());
return address; return address;

View File

@ -21,9 +21,7 @@ import java.io.IOException;
import static ch.dissem.bitmessage.utils.UnixTime.DAY; import static ch.dissem.bitmessage.utils.UnixTime.DAY;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -82,7 +80,7 @@ public class CryptographyTest {
.nonce(new byte[8]) .nonce(new byte[8])
.expiresTime(UnixTime.now(+28 * DAY)) .expiresTime(UnixTime.now(+28 * DAY))
.objectType(0) .objectType(0)
.payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0))
.build(); .build();
crypto.checkProofOfWork(objectMessage, 1000, 1000); crypto.checkProofOfWork(objectMessage, 1000, 1000);
} }
@ -93,7 +91,7 @@ public class CryptographyTest {
.nonce(new byte[8]) .nonce(new byte[8])
.expiresTime(UnixTime.now(+2 * MINUTE)) .expiresTime(UnixTime.now(+2 * MINUTE))
.objectType(0) .objectType(0)
.payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0))
.build(); .build();
final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>();
crypto.doProofOfWork(objectMessage, 1000, 1000, crypto.doProofOfWork(objectMessage, 1000, 1000,

View File

@ -31,6 +31,7 @@ dependencies {
compile 'org.slf4j:slf4j-simple:1.7.12' compile 'org.slf4j:slf4j-simple:1.7.12'
compile 'args4j:args4j:2.32' compile 'args4j:args4j:2.32'
compile 'com.h2database:h2:1.4.190' compile 'com.h2database:h2:1.4.190'
compile 'org.apache.commons:commons-lang3:3.4'
testCompile 'junit:junit:4.11' testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'
} }

View File

@ -25,10 +25,10 @@ import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.MemoryNodeRegistry;
import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.repository.*;
import org.apache.commons.lang3.text.WordUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -56,13 +56,7 @@ public class Application {
.networkHandler(new DefaultNetworkHandler()) .networkHandler(new DefaultNetworkHandler())
.cryptography(new BouncyCryptography()) .cryptography(new BouncyCryptography())
.port(48444) .port(48444)
.listener(plaintext -> { .listener(plaintext -> System.out.println("New Message from " + plaintext.getFrom() + ": " + plaintext.getSubject()))
try {
System.out.println(new String(plaintext.getMessage(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
LOG.error(e.getMessage(), e);
}
})
.build(); .build();
if (syncServer == null) { if (syncServer == null) {
@ -99,7 +93,7 @@ public class Application {
subscriptions(); subscriptions();
break; break;
case "m": case "m":
messages(); labels();
break; break;
case "?": case "?":
info(); info();
@ -124,8 +118,27 @@ public class Application {
} }
private void info() { private void info() {
String command;
do {
System.out.println(); System.out.println();
System.out.println(ctx.status()); System.out.println(ctx.status());
System.out.println();
System.out.println("c) cleanup inventory");
System.out.println("r) resend unacknowledged messages");
System.out.println(COMMAND_BACK);
command = commandLine.nextCommand();
switch (command) {
case "c":
ctx.cleanup();
break;
case "r":
ctx.resendUnacknowledgedMessages();
break;
case "b":
return;
}
} while (!"b".equals(command));
} }
private void identities() { private void identities() {
@ -280,22 +293,71 @@ public class Application {
} }
} }
private void messages() { private void labels() {
List<Label> labels = ctx.messages().getLabels();
String command; String command;
do { do {
List<Plaintext> messages = ctx.messages().findMessages(Plaintext.Status.RECEIVED); System.out.println();
int i = 0;
for (Label label : labels) {
i++;
System.out.print(i + ") " + label);
int unread = ctx.messages().countUnread(label);
if (unread > 0) {
System.out.println(" [" + unread + "]");
} else {
System.out.println();
}
}
System.out.println("a) Archive");
System.out.println();
System.out.println("c) compose message");
System.out.println("s) compose broadcast");
System.out.println(COMMAND_BACK);
command = commandLine.nextCommand();
switch (command) {
case "a":
messages(null);
break;
case "c":
compose(false);
break;
case "s":
compose(true);
break;
case "b":
return;
default:
try {
int index = Integer.parseInt(command) - 1;
messages(labels.get(index));
} catch (NumberFormatException | IndexOutOfBoundsException e) {
System.out.println(ERROR_UNKNOWN_COMMAND);
}
}
} while (!"b".equalsIgnoreCase(command));
}
private void messages(Label label) {
String command;
do {
List<Plaintext> messages = ctx.messages().findMessages(label);
System.out.println(); System.out.println();
int i = 0; int i = 0;
for (Plaintext message : messages) { for (Plaintext message : messages) {
i++; i++;
System.out.println(i + ") From: " + message.getFrom() + "; Subject: " + message.getSubject()); System.out.println(i + (message.isUnread() ? ">" : ")") + " From: " + message.getFrom() + "; Subject: " + message.getSubject());
} }
if (i == 0) { if (i == 0) {
System.out.println("You have no messages."); System.out.println("There are no messages.");
} }
System.out.println(); System.out.println();
System.out.println("c) compose message"); System.out.println("c) compose message");
System.out.println("s) compose broadcast"); System.out.println("s) compose broadcast");
if (label.getType() == Label.Type.TRASH) {
System.out.println("e) empty trash");
}
System.out.println(COMMAND_BACK); System.out.println(COMMAND_BACK);
command = commandLine.nextCommand(); command = commandLine.nextCommand();
@ -306,6 +368,8 @@ public class Application {
case "s": case "s":
compose(true); compose(true);
break; break;
case "e":
messages.forEach(ctx.messages()::remove);
case "b": case "b":
return; return;
default: default:
@ -325,16 +389,18 @@ public class Application {
System.out.println("To: " + message.getTo()); System.out.println("To: " + message.getTo());
System.out.println("Subject: " + message.getSubject()); System.out.println("Subject: " + message.getSubject());
System.out.println(); System.out.println();
System.out.println(message.getText()); System.out.println(WordUtils.wrap(message.getText(), 120));
System.out.println(); System.out.println();
System.out.println(message.getLabels().stream().map(Label::toString).collect( System.out.println(message.getLabels().stream().map(Label::toString).collect(
Collectors.joining("Labels: ", ", ", ""))); Collectors.joining(", ", "Labels: ", "")));
System.out.println(); System.out.println();
ctx.labeler().markAsRead(message); ctx.labeler().markAsRead(message);
ctx.messages().save(message);
String command; String command;
do { do {
System.out.println("r) reply"); System.out.println("r) reply");
System.out.println("d) delete"); System.out.println("d) delete");
System.out.println("a) archive");
System.out.println(COMMAND_BACK); System.out.println(COMMAND_BACK);
command = commandLine.nextCommand(); command = commandLine.nextCommand();
switch (command) { switch (command) {
@ -342,7 +408,13 @@ public class Application {
compose(message.getTo(), message.getFrom(), "RE: " + message.getSubject()); compose(message.getTo(), message.getFrom(), "RE: " + message.getSubject());
break; break;
case "d": case "d":
ctx.messages().remove(message); ctx.labeler().delete(message);
ctx.messages().save(message);
return;
case "a":
ctx.labeler().archive(message);
ctx.messages().save(message);
return;
case "b": case "b":
return; return;
default: default:

View File

@ -4,16 +4,25 @@ import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
import ch.dissem.bitmessage.ports.DefaultLabeler;
import ch.dissem.bitmessage.ports.Labeler;
import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.repository.*;
import ch.dissem.bitmessage.utils.TTL; import ch.dissem.bitmessage.utils.TTL;
import org.junit.*; import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
/** /**
* @author Christian Basler * @author Christian Basler
@ -23,6 +32,7 @@ public class SystemTest {
private BitmessageContext alice; private BitmessageContext alice;
private TestListener aliceListener = new TestListener(); private TestListener aliceListener = new TestListener();
private Labeler aliceLabeler = Mockito.spy(new DebugLabeler("Alice"));
private BitmessageAddress aliceIdentity; private BitmessageAddress aliceIdentity;
private BitmessageContext bob; private BitmessageContext bob;
@ -47,9 +57,10 @@ public class SystemTest {
.networkHandler(new DefaultNetworkHandler()) .networkHandler(new DefaultNetworkHandler())
.cryptography(new BouncyCryptography()) .cryptography(new BouncyCryptography())
.listener(aliceListener) .listener(aliceListener)
.labeler(aliceLabeler)
.build(); .build();
alice.startup(); alice.startup();
aliceIdentity = alice.createIdentity(false); aliceIdentity = alice.createIdentity(false, DOES_ACK);
JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", "");
bob = new BitmessageContext.Builder() bob = new BitmessageContext.Builder()
@ -62,9 +73,13 @@ public class SystemTest {
.networkHandler(new DefaultNetworkHandler()) .networkHandler(new DefaultNetworkHandler())
.cryptography(new BouncyCryptography()) .cryptography(new BouncyCryptography())
.listener(bobListener) .listener(bobListener)
.labeler(new DebugLabeler("Bob"))
.build(); .build();
bob.startup(); bob.startup();
bobIdentity = bob.createIdentity(false); bobIdentity = bob.createIdentity(false, DOES_ACK);
((DebugLabeler) alice.labeler()).init(aliceIdentity, bobIdentity);
((DebugLabeler) bob.labeler()).init(aliceIdentity, bobIdentity);
} }
@After @After
@ -82,6 +97,9 @@ public class SystemTest {
assertThat(plaintext.getType(), equalTo(Plaintext.Type.MSG)); assertThat(plaintext.getType(), equalTo(Plaintext.Type.MSG));
assertThat(plaintext.getText(), equalTo(originalMessage)); assertThat(plaintext.getText(), equalTo(originalMessage));
Mockito.verify(aliceLabeler, Mockito.timeout(TimeUnit.MINUTES.toMillis(15)).atLeastOnce())
.markAsAcknowledged(any());
} }
@Test @Test
@ -95,4 +113,83 @@ public class SystemTest {
assertThat(plaintext.getType(), equalTo(Plaintext.Type.BROADCAST)); assertThat(plaintext.getType(), equalTo(Plaintext.Type.BROADCAST));
assertThat(plaintext.getText(), equalTo(originalMessage)); assertThat(plaintext.getText(), equalTo(originalMessage));
} }
private static class DebugLabeler extends DefaultLabeler {
private final Logger LOG = LoggerFactory.getLogger("Labeler");
final String name;
String alice;
String bob;
private DebugLabeler(String name) {
this.name = name;
}
private void init(BitmessageAddress alice, BitmessageAddress bob) {
this.alice = alice.getAddress();
this.bob = bob.getAddress();
}
@Override
public void setLabels(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Received");
super.setLabels(msg);
}
@Override
public void markAsDraft(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Draft");
super.markAsDraft(msg);
}
@Override
public void markAsSending(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Sending");
super.markAsSending(msg);
}
@Override
public void markAsSent(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Sent");
super.markAsSent(msg);
}
@Override
public void markAsAcknowledged(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Acknowledged");
super.markAsAcknowledged(msg);
}
@Override
public void markAsRead(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Read");
super.markAsRead(msg);
}
@Override
public void markAsUnread(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Unread");
super.markAsUnread(msg);
}
@Override
public void delete(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Cleared");
super.delete(msg);
}
@Override
public void archive(Plaintext msg) {
LOG.info(name + ": From " + name(msg.getFrom()) + ": Archived");
super.archive(msg);
}
private String name(BitmessageAddress address) {
if (alice.equals(address.getAddress()))
return "Alice";
else if (bob.equals(address.getAddress()))
return "Bob";
else
return "Unknown (" + address.getAddress() + ")";
}
}
} }

View File

@ -28,7 +28,7 @@ import ch.dissem.bitmessage.utils.Encode;
import java.io.*; import java.io.*;
import static ch.dissem.bitmessage.utils.Decode.*; import static ch.dissem.bitmessage.utils.Decode.*;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* A {@link CustomMessage} implementation that contains signed and encrypted data. * A {@link CustomMessage} implementation that contains signed and encrypted data.
@ -80,7 +80,7 @@ public class CryptoCustomMessage<T extends Streamable> extends CustomMessage {
} }
data.write(out); data.write(out);
Encode.varBytes(security().getSignature(out.toByteArray(), identity.getPrivateKey()), out); Encode.varBytes(cryptography().getSignature(out.toByteArray(), identity.getPrivateKey()), out);
container = new CryptoBox(out.toByteArray(), publicKey); container = new CryptoBox(out.toByteArray(), publicKey);
} }
@ -138,7 +138,7 @@ public class CryptoCustomMessage<T extends Streamable> extends CustomMessage {
} }
public void checkSignature(Pubkey pubkey) throws IOException, IllegalStateException { public void checkSignature(Pubkey pubkey) throws IOException, IllegalStateException {
if (!security().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { if (!cryptography().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) {
throw new IllegalStateException("Signature check failed"); throw new IllegalStateException("Signature check failed");
} }
} }

View File

@ -30,7 +30,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class CryptoCustomMessageTest extends TestBase { public class CryptoCustomMessageTest extends TestBase {
@ -39,9 +39,9 @@ public class CryptoCustomMessageTest extends TestBase {
PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey"));
BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey); BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey);
GenericPayload payloadBefore = new GenericPayload(0, 1, security().randomBytes(100)); GenericPayload payloadBefore = new GenericPayload(0, 1, cryptography().randomBytes(100));
CryptoCustomMessage<GenericPayload> messageBefore = new CryptoCustomMessage<>(payloadBefore); CryptoCustomMessage<GenericPayload> messageBefore = new CryptoCustomMessage<>(payloadBefore);
messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey())); messageBefore.signAndEncrypt(sendingIdentity, cryptography().createPublicKey(sendingIdentity.getPublicDecryptionKey()));
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
messageBefore.write(out); messageBefore.write(out);
@ -52,7 +52,7 @@ public class CryptoCustomMessageTest extends TestBase {
new CryptoCustomMessage.Reader<GenericPayload>() { new CryptoCustomMessage.Reader<GenericPayload>() {
@Override @Override
public GenericPayload read(BitmessageAddress ignore, InputStream in) throws IOException { public GenericPayload read(BitmessageAddress ignore, InputStream in) throws IOException {
return GenericPayload.read(0, in, 1, 100); return GenericPayload.read(0, 1, in, 100);
} }
}); });
GenericPayload payloadAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey()); GenericPayload payloadAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey());
@ -65,11 +65,11 @@ public class CryptoCustomMessageTest extends TestBase {
PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey"));
final BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey); final BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey);
ProofOfWorkRequest requestBefore = new ProofOfWorkRequest(sendingIdentity, security().randomBytes(64), ProofOfWorkRequest requestBefore = new ProofOfWorkRequest(sendingIdentity, cryptography().randomBytes(64),
ProofOfWorkRequest.Request.CALCULATE); ProofOfWorkRequest.Request.CALCULATE);
CryptoCustomMessage<ProofOfWorkRequest> messageBefore = new CryptoCustomMessage<>(requestBefore); CryptoCustomMessage<ProofOfWorkRequest> messageBefore = new CryptoCustomMessage<>(requestBefore);
messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey())); messageBefore.signAndEncrypt(sendingIdentity, cryptography().createPublicKey(sendingIdentity.getPublicDecryptionKey()));
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();

View File

@ -46,7 +46,7 @@ import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE
import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT; import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT;
import static ch.dissem.bitmessage.networking.Connection.Mode.SYNC; import static ch.dissem.bitmessage.networking.Connection.Mode.SYNC;
import static ch.dissem.bitmessage.networking.Connection.State.*; import static ch.dissem.bitmessage.networking.Connection.State.*;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
/** /**
@ -72,6 +72,7 @@ class Connection {
private final ReaderRunnable reader = new ReaderRunnable(); private final ReaderRunnable reader = new ReaderRunnable();
private final WriterRunnable writer = new WriterRunnable(); private final WriterRunnable writer = new WriterRunnable();
private final DefaultNetworkHandler networkHandler; private final DefaultNetworkHandler networkHandler;
private final long clientNonce;
private volatile State state; private volatile State state;
private InputStream in; private InputStream in;
@ -83,22 +84,23 @@ class Connection {
private long lastObjectTime; private long lastObjectTime;
public Connection(InternalContext context, Mode mode, Socket socket, MessageListener listener, public Connection(InternalContext context, Mode mode, Socket socket, MessageListener listener,
Set<InventoryVector> requestedObjectsMap) throws IOException { Set<InventoryVector> requestedObjectsMap, long clientNonce) throws IOException {
this(context, mode, listener, socket, requestedObjectsMap, this(context, mode, listener, socket, requestedObjectsMap,
Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)), Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)),
new NetworkAddress.Builder().ip(socket.getInetAddress()).port(socket.getPort()).stream(1).build(), new NetworkAddress.Builder().ip(socket.getInetAddress()).port(socket.getPort()).stream(1).build(),
0); 0, clientNonce);
} }
public Connection(InternalContext context, Mode mode, NetworkAddress node, MessageListener listener, public Connection(InternalContext context, Mode mode, NetworkAddress node, MessageListener listener,
Set<InventoryVector> requestedObjectsMap) { Set<InventoryVector> requestedObjectsMap, long clientNonce) {
this(context, mode, listener, new Socket(), requestedObjectsMap, this(context, mode, listener, new Socket(), requestedObjectsMap,
Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)), Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)),
node, 0); node, 0, clientNonce);
} }
private Connection(InternalContext context, Mode mode, MessageListener listener, Socket socket, private Connection(InternalContext context, Mode mode, MessageListener listener, Socket socket,
Set<InventoryVector> commonRequestedObjects, Set<InventoryVector> requestedObjects, NetworkAddress node, long syncTimeout) { Set<InventoryVector> commonRequestedObjects, Set<InventoryVector> requestedObjects,
NetworkAddress node, long syncTimeout, long clientNonce) {
this.startTime = UnixTime.now(); this.startTime = UnixTime.now();
this.ctx = context; this.ctx = context;
this.mode = mode; this.mode = mode;
@ -112,6 +114,7 @@ class Connection {
this.syncTimeout = (syncTimeout > 0 ? UnixTime.now(+syncTimeout) : 0); this.syncTimeout = (syncTimeout > 0 ? UnixTime.now(+syncTimeout) : 0);
this.ivCache = new ConcurrentHashMap<>(); this.ivCache = new ConcurrentHashMap<>();
this.networkHandler = (DefaultNetworkHandler) ctx.getNetworkHandler(); this.networkHandler = (DefaultNetworkHandler) ctx.getNetworkHandler();
this.clientNonce = clientNonce;
} }
public static Connection sync(InternalContext ctx, InetAddress address, int port, MessageListener listener, public static Connection sync(InternalContext ctx, InetAddress address, int port, MessageListener listener,
@ -120,7 +123,7 @@ class Connection {
new HashSet<InventoryVector>(), new HashSet<InventoryVector>(),
new HashSet<InventoryVector>(), new HashSet<InventoryVector>(),
new NetworkAddress.Builder().ip(address).port(port).stream(1).build(), new NetworkAddress.Builder().ip(address).port(port).stream(1).build(),
timeoutInSeconds); timeoutInSeconds, cryptography().randomNonce());
} }
public long getStartTime() { public long getStartTime() {
@ -249,7 +252,7 @@ class Connection {
} }
try { try {
listener.receive(objectMessage); listener.receive(objectMessage);
security().checkProofOfWork(objectMessage, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES); cryptography().checkProofOfWork(objectMessage, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES);
ctx.getInventory().storeObject(objectMessage); ctx.getInventory().storeObject(objectMessage);
// offer object to some random nodes so it gets distributed throughout the network: // offer object to some random nodes so it gets distributed throughout the network:
networkHandler.offer(objectMessage.getInventoryVector()); networkHandler.offer(objectMessage.getInventoryVector());
@ -362,7 +365,7 @@ class Connection {
try (Socket socket = Connection.this.socket) { try (Socket socket = Connection.this.socket) {
initSocket(socket); initSocket(socket);
if (mode == CLIENT || mode == SYNC) { if (mode == CLIENT || mode == SYNC) {
send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); send(new Version.Builder().defaults(clientNonce).addrFrom(host).addrRecv(node).build());
} }
while (state != DISCONNECTED) { while (state != DISCONNECTED) {
if (mode != SYNC) { if (mode != SYNC) {
@ -446,7 +449,7 @@ class Connection {
send(new VerAck()); send(new VerAck());
switch (mode) { switch (mode) {
case SERVER: case SERVER:
send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); send(new Version.Builder().defaults(clientNonce).addrFrom(host).addrRecv(node).build());
break; break;
case CLIENT: case CLIENT:
case SYNC: case SYNC:

View File

@ -38,15 +38,17 @@ public class ConnectionOrganizer implements Runnable {
private final InternalContext ctx; private final InternalContext ctx;
private final DefaultNetworkHandler networkHandler; private final DefaultNetworkHandler networkHandler;
private final NetworkHandler.MessageListener listener; private final NetworkHandler.MessageListener listener;
private final long clientNonce;
private Connection initialConnection; private Connection initialConnection;
public ConnectionOrganizer(InternalContext ctx, public ConnectionOrganizer(InternalContext ctx,
DefaultNetworkHandler networkHandler, DefaultNetworkHandler networkHandler,
NetworkHandler.MessageListener listener) { NetworkHandler.MessageListener listener, long clientNonce) {
this.ctx = ctx; this.ctx = ctx;
this.networkHandler = networkHandler; this.networkHandler = networkHandler;
this.listener = listener; this.listener = listener;
this.clientNonce = clientNonce;
} }
@Override @Override
@ -91,7 +93,8 @@ public class ConnectionOrganizer implements Runnable {
NETWORK_MAGIC_NUMBER - active, ctx.getStreams()); NETWORK_MAGIC_NUMBER - active, ctx.getStreams());
boolean first = active == 0 && initialConnection == null; boolean first = active == 0 && initialConnection == null;
for (NetworkAddress address : addresses) { for (NetworkAddress address : addresses) {
Connection c = new Connection(ctx, CLIENT, address, listener, networkHandler.requestedObjects); Connection c = new Connection(ctx, CLIENT, address, listener,
networkHandler.requestedObjects, clientNonce);
if (first) { if (first) {
initialConnection = c; initialConnection = c;
first = false; first = false;

View File

@ -28,7 +28,6 @@ import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.NetworkHandler; import ch.dissem.bitmessage.ports.NetworkHandler;
import ch.dissem.bitmessage.utils.Collections; import ch.dissem.bitmessage.utils.Collections;
import ch.dissem.bitmessage.utils.Property; import ch.dissem.bitmessage.utils.Property;
import ch.dissem.bitmessage.utils.ThreadFactoryBuilder;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
@ -109,9 +108,9 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder {
try { try {
running = true; running = true;
connections.clear(); connections.clear();
server = new ServerRunnable(ctx, this, listener); server = new ServerRunnable(ctx, this, listener, ctx.getClientNonce());
pool.execute(server); pool.execute(server);
pool.execute(new ConnectionOrganizer(ctx, this, listener)); pool.execute(new ConnectionOrganizer(ctx, this, listener, ctx.getClientNonce()));
} catch (IOException e) { } catch (IOException e) {
throw new ApplicationException(e); throw new ApplicationException(e);
} }

View File

@ -37,12 +37,15 @@ public class ServerRunnable implements Runnable, Closeable {
private final ServerSocket serverSocket; private final ServerSocket serverSocket;
private final DefaultNetworkHandler networkHandler; private final DefaultNetworkHandler networkHandler;
private final NetworkHandler.MessageListener listener; private final NetworkHandler.MessageListener listener;
private final long clientNonce;
public ServerRunnable(InternalContext ctx, DefaultNetworkHandler networkHandler, NetworkHandler.MessageListener listener) throws IOException { public ServerRunnable(InternalContext ctx, DefaultNetworkHandler networkHandler,
NetworkHandler.MessageListener listener, long clientNonce) throws IOException {
this.ctx = ctx; this.ctx = ctx;
this.networkHandler = networkHandler; this.networkHandler = networkHandler;
this.listener = listener; this.listener = listener;
this.serverSocket = new ServerSocket(ctx.getPort()); this.serverSocket = new ServerSocket(ctx.getPort());
this.clientNonce = clientNonce;
} }
@Override @Override
@ -52,7 +55,7 @@ public class ServerRunnable implements Runnable, Closeable {
Socket socket = serverSocket.accept(); Socket socket = serverSocket.accept();
socket.setSoTimeout(Connection.READ_TIMEOUT); socket.setSoTimeout(Connection.READ_TIMEOUT);
networkHandler.startConnection(new Connection(ctx, SERVER, socket, listener, networkHandler.startConnection(new Connection(ctx, SERVER, socket, listener,
networkHandler.requestedObjects)); networkHandler.requestedObjects, clientNonce));
} catch (IOException e) { } catch (IOException e) {
LOG.debug(e.getMessage(), e); LOG.debug(e.getMessage(), e);
} }

View File

@ -18,5 +18,6 @@ dependencies {
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'com.h2database:h2:1.4.190' testCompile 'com.h2database:h2:1.4.190'
testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'
testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile project(':cryptography-bc') testCompile project(':cryptography-bc')
} }

View File

@ -18,6 +18,7 @@ package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.exception.ApplicationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -25,6 +26,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Collection;
import static ch.dissem.bitmessage.utils.Strings.hex; import static ch.dissem.bitmessage.utils.Strings.hex;
@ -40,43 +42,7 @@ public abstract class JdbcHelper {
this.config = config; this.config = config;
} }
public static StringBuilder join(long... objects) { public static void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(objects[i]);
}
return streamList;
}
public static StringBuilder join(byte[]... objects) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(hex(objects[i]));
}
return streamList;
}
public static StringBuilder join(ObjectType... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(types[i].getNumber());
}
return streamList;
}
public static StringBuilder join(Enum... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append('\'').append(types[i].name()).append('\'');
}
return streamList;
}
protected void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException {
if (data == null) { if (data == null) {
ps.setBytes(parameterIndex, null); ps.setBytes(parameterIndex, null);
} else { } else {

View File

@ -31,6 +31,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import static ch.dissem.bitmessage.utils.SqlStrings.join;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static ch.dissem.bitmessage.utils.UnixTime.now; import static ch.dissem.bitmessage.utils.UnixTime.now;

View File

@ -16,14 +16,12 @@
package ch.dissem.bitmessage.repository; package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.ports.AbstractMessageRepository;
import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Strings;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -31,34 +29,30 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.sql.*; import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public class JdbcMessageRepository extends JdbcHelper implements MessageRepository, InternalContext.ContextHolder { import static ch.dissem.bitmessage.repository.JdbcHelper.writeBlob;
public class JdbcMessageRepository extends AbstractMessageRepository implements MessageRepository {
private static final Logger LOG = LoggerFactory.getLogger(JdbcMessageRepository.class); private static final Logger LOG = LoggerFactory.getLogger(JdbcMessageRepository.class);
private InternalContext ctx; private final JdbcConfig config;
public JdbcMessageRepository(JdbcConfig config) { public JdbcMessageRepository(JdbcConfig config) {
super(config); this.config = config;
} }
@Override @Override
public List<Label> getLabels() { protected List<Label> findLabels(String where) {
List<Label> result = new LinkedList<>();
try ( try (
Connection connection = config.getConnection(); Connection connection = config.getConnection()
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label ORDER BY ord")
) { ) {
while (rs.next()) { return findLabels(connection, where);
result.add(getLabel(rs));
}
} catch (SQLException e) { } catch (SQLException e) {
throw new ApplicationException(e); LOG.error(e.getMessage(), e);
} }
return result; return new ArrayList<>();
} }
private Label getLabel(ResultSet rs) throws SQLException { private Label getLabel(ResultSet rs) throws SQLException {
@ -73,24 +67,6 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
return label; return label;
} }
@Override
public List<Label> getLabels(Label.Type... types) {
List<Label> result = new LinkedList<>();
try (
Connection connection = config.getConnection();
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE type IN (" + join(types) +
") ORDER BY ord")
) {
while (rs.next()) {
result.add(getLabel(rs));
}
} catch (SQLException e) {
LOG.error(e.getMessage(), e);
}
return result;
}
@Override @Override
public int countUnread(Label label) { public int countUnread(Label label) {
String where; String where;
@ -105,7 +81,8 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
try ( try (
Connection connection = config.getConnection(); Connection connection = config.getConnection();
Statement stmt = connection.createStatement(); Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where) ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where
+ " ORDER BY received DESC")
) { ) {
if (rs.next()) { if (rs.next()) {
return rs.getInt(1); return rs.getInt(1);
@ -117,45 +94,13 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
} }
@Override @Override
public Plaintext getMessage(byte[] initialHash) { protected List<Plaintext> find(String where) {
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 ApplicationException("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() + ")");
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) {
return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'");
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status) {
return find("status='" + status.name() + "'");
}
@Override
public List<Plaintext> findMessages(BitmessageAddress sender) {
return find("sender='" + sender.getAddress() + "'");
}
private List<Plaintext> find(String where) {
List<Plaintext> result = new LinkedList<>(); List<Plaintext> result = new LinkedList<>();
try ( try (
Connection connection = config.getConnection(); Connection connection = config.getConnection();
Statement stmt = connection.createStatement(); Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, iv, type, sender, recipient, data, sent, received, status " + ResultSet rs = stmt.executeQuery(
"SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try " +
"FROM Message WHERE " + where) "FROM Message WHERE " + where)
) { ) {
while (rs.next()) { while (rs.next()) {
@ -168,11 +113,18 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
builder.IV(new InventoryVector(iv)); builder.IV(new InventoryVector(iv));
builder.from(ctx.getAddressRepository().getAddress(rs.getString("sender"))); builder.from(ctx.getAddressRepository().getAddress(rs.getString("sender")));
builder.to(ctx.getAddressRepository().getAddress(rs.getString("recipient"))); builder.to(ctx.getAddressRepository().getAddress(rs.getString("recipient")));
builder.ackData(rs.getBytes("ack_data"));
builder.sent(rs.getLong("sent")); builder.sent(rs.getLong("sent"));
builder.received(rs.getLong("received")); builder.received(rs.getLong("received"));
builder.status(Plaintext.Status.valueOf(rs.getString("status"))); builder.status(Plaintext.Status.valueOf(rs.getString("status")));
builder.labels(findLabels(connection, id)); builder.ttl(rs.getLong("ttl"));
result.add(builder.build()); builder.retries(rs.getInt("retries"));
builder.nextTry(rs.getLong("next_try"));
builder.labels(findLabels(connection,
"WHERE id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ") ORDER BY ord"));
Plaintext message = builder.build();
message.setInitialHash(rs.getBytes("initial_hash"));
result.add(message);
} }
} catch (IOException | SQLException e) { } catch (IOException | SQLException e) {
LOG.error(e.getMessage(), e); LOG.error(e.getMessage(), e);
@ -180,12 +132,11 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
return result; return result;
} }
private Collection<Label> findLabels(Connection connection, long messageId) { private List<Label> findLabels(Connection connection, String where) {
List<Label> result = new ArrayList<>(); List<Label> result = new ArrayList<>();
try ( try (
Statement stmt = connection.createStatement(); Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label " + ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE " + where)
"WHERE id IN (SELECT label_id FROM Message_Label WHERE message_id=" + messageId + ")")
) { ) {
while (rs.next()) { while (rs.next()) {
result.add(getLabel(rs)); result.add(getLabel(rs));
@ -198,16 +149,7 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
@Override @Override
public void save(Plaintext message) { public void save(Plaintext message) {
// save from address if necessary safeSenderIfNecessary(message);
if (message.getId() == null) {
BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress());
if (savedAddress == null) {
ctx.getAddressRepository().save(message.getFrom());
} else if (savedAddress.getPubkey() == null && message.getFrom().getPubkey() != null) {
savedAddress.setPubkey(message.getFrom().getPubkey());
ctx.getAddressRepository().save(savedAddress);
}
}
try (Connection connection = config.getConnection()) { try (Connection connection = config.getConnection()) {
try { try {
@ -249,8 +191,9 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
private void insert(Connection connection, Plaintext message) throws SQLException, IOException { private void insert(Connection connection, Plaintext message) throws SQLException, IOException {
try (PreparedStatement ps = connection.prepareStatement( try (PreparedStatement ps = connection.prepareStatement(
"INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status, initial_hash) " + "INSERT INTO Message (iv, type, sender, recipient, data, ack_data, sent, received, " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", "status, initial_hash, ttl, retries, next_try) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS) Statement.RETURN_GENERATED_KEYS)
) { ) {
ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash());
@ -258,10 +201,14 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
ps.setString(3, message.getFrom().getAddress()); ps.setString(3, message.getFrom().getAddress());
ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress()); ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress());
writeBlob(ps, 5, message); writeBlob(ps, 5, message);
ps.setLong(6, message.getSent()); ps.setBytes(6, message.getAckData());
ps.setLong(7, message.getReceived()); ps.setLong(7, message.getSent());
ps.setString(8, message.getStatus() == null ? null : message.getStatus().name()); ps.setLong(8, message.getReceived());
ps.setBytes(9, message.getInitialHash()); ps.setString(9, message.getStatus() == null ? null : message.getStatus().name());
ps.setBytes(10, message.getInitialHash());
ps.setLong(11, message.getTTL());
ps.setInt(12, message.getRetries());
ps.setObject(13, message.getNextTry());
ps.executeUpdate(); ps.executeUpdate();
// get generated id // get generated id
@ -274,13 +221,23 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
private void update(Connection connection, Plaintext message) throws SQLException, IOException { private void update(Connection connection, Plaintext message) throws SQLException, IOException {
try (PreparedStatement ps = connection.prepareStatement( try (PreparedStatement ps = connection.prepareStatement(
"UPDATE Message SET iv=?, sent=?, received=?, status=?, initial_hash=? WHERE id=?")) { "UPDATE Message SET iv=?, type=?, sender=?, recipient=?, data=?, ack_data=?, sent=?, received=?, " +
"status=?, initial_hash=?, ttl=?, retries=?, next_try=? " +
"WHERE id=?")) {
ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash());
ps.setLong(2, message.getSent()); ps.setString(2, message.getType().name());
ps.setLong(3, message.getReceived()); ps.setString(3, message.getFrom().getAddress());
ps.setString(4, message.getStatus() == null ? null : message.getStatus().name()); ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress());
ps.setBytes(5, message.getInitialHash()); writeBlob(ps, 5, message);
ps.setLong(6, (Long) message.getId()); ps.setBytes(6, message.getAckData());
ps.setLong(7, message.getSent());
ps.setLong(8, message.getReceived());
ps.setString(9, message.getStatus() == null ? null : message.getStatus().name());
ps.setBytes(10, message.getInitialHash());
ps.setLong(11, message.getTTL());
ps.setInt(12, message.getRetries());
ps.setObject(13, message.getNextTry());
ps.setLong(14, (Long) message.getId());
ps.executeUpdate(); ps.executeUpdate();
} }
} }
@ -305,9 +262,4 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
LOG.error(e.getMessage(), e); LOG.error(e.getMessage(), e);
} }
} }
@Override
public void setContext(InternalContext context) {
this.ctx = context;
}
} }

View File

@ -1,6 +1,8 @@
package ch.dissem.bitmessage.repository; package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
@ -8,18 +10,21 @@ import ch.dissem.bitmessage.utils.Strings;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.sql.*; import java.sql.*;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository { public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository, InternalContext.ContextHolder {
private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class); private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class);
private InternalContext ctx;
public JdbcProofOfWorkRepository(JdbcConfig config) { public JdbcProofOfWorkRepository(JdbcConfig config) {
super(config); super(config);
@ -30,17 +35,27 @@ public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWork
try ( try (
Connection connection = config.getConnection(); Connection connection = config.getConnection();
PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, " + PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, " +
"extra_bytes FROM POW WHERE initial_hash=?") "extra_bytes, expiration_time, message_id FROM POW WHERE initial_hash=?")
) { ) {
ps.setBytes(1, initialHash); ps.setBytes(1, initialHash);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) { if (rs.next()) {
Blob data = rs.getBlob("data"); Blob data = rs.getBlob("data");
if (rs.getObject("message_id") == null) {
return new Item( return new Item(
Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()),
rs.getLong("nonce_trials_per_byte"), rs.getLong("nonce_trials_per_byte"),
rs.getLong("extra_bytes") rs.getLong("extra_bytes")
); );
} else {
return new Item(
Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()),
rs.getLong("nonce_trials_per_byte"),
rs.getLong("extra_bytes"),
rs.getLong("expiration_time"),
ctx.getMessageRepository().getMessage(rs.getLong("message_id"))
);
}
} else { } else {
throw new IllegalArgumentException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); throw new IllegalArgumentException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash));
} }
@ -70,24 +85,38 @@ public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWork
} }
@Override @Override
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { public void putObject(Item item) {
try ( try (
Connection connection = config.getConnection(); Connection connection = config.getConnection();
PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, " + PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, " +
"nonce_trials_per_byte, extra_bytes) VALUES (?, ?, ?, ?, ?)") "nonce_trials_per_byte, extra_bytes, expiration_time, message_id) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)")
) { ) {
ps.setBytes(1, security().getInitialHash(object)); ps.setBytes(1, cryptography().getInitialHash(item.object));
writeBlob(ps, 2, object); writeBlob(ps, 2, item.object);
ps.setLong(3, object.getVersion()); ps.setLong(3, item.object.getVersion());
ps.setLong(4, nonceTrialsPerByte); ps.setLong(4, item.nonceTrialsPerByte);
ps.setLong(5, extraBytes); ps.setLong(5, item.extraBytes);
if (item.message == null) {
ps.setObject(6, null);
ps.setObject(7, null);
} else {
ps.setLong(6, item.expirationTime);
ps.setLong(7, (Long) item.message.getId());
}
ps.executeUpdate(); ps.executeUpdate();
} catch (IOException | SQLException e) { } catch (IOException | SQLException e) {
LOG.debug("Error storing object of type " + object.getPayload().getClass().getSimpleName(), e); LOG.debug("Error storing object of type " + item.object.getPayload().getClass().getSimpleName(), e);
throw new ApplicationException(e); throw new ApplicationException(e);
} }
} }
@Override
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
putObject(new Item(object, nonceTrialsPerByte, extraBytes));
}
@Override @Override
public void removeObject(byte[] initialHash) { public void removeObject(byte[] initialHash) {
try ( try (
@ -100,4 +129,9 @@ public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWork
LOG.debug(e.getMessage(), e); LOG.debug(e.getMessage(), e);
} }
} }
@Override
public void setContext(InternalContext context) {
this.ctx = context;
}
} }

View File

@ -0,0 +1,2 @@
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
ALTER TABLE POW ADD COLUMN message_id BIGINT;

View File

@ -0,0 +1,4 @@
ALTER TABLE Message ADD COLUMN ack_data BINARY(32);
ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0;
ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0;
ALTER TABLE Message ADD COLUMN next_try BIGINT;

View File

@ -17,12 +17,14 @@
package ch.dissem.bitmessage.repository; package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.List; import java.util.List;
import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class JdbcAddressRepositoryTest extends TestBase { public class JdbcAddressRepositoryTest extends TestBase {
@ -46,7 +48,7 @@ public class JdbcAddressRepositoryTest extends TestBase {
repo.save(new BitmessageAddress(CONTACT_B)); repo.save(new BitmessageAddress(CONTACT_B));
repo.save(new BitmessageAddress(CONTACT_C)); repo.save(new BitmessageAddress(CONTACT_C));
BitmessageAddress identityA = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); BitmessageAddress identityA = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK));
repo.save(identityA); repo.save(identityA);
IDENTITY_A = identityA.getAddress(); IDENTITY_A = identityA.getAddress();
BitmessageAddress identityB = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); BitmessageAddress identityB = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000));
@ -66,8 +68,11 @@ public class JdbcAddressRepositoryTest extends TestBase {
public void testFindIdentity() throws Exception { public void testFindIdentity() throws Exception {
BitmessageAddress identity = new BitmessageAddress(IDENTITY_A); BitmessageAddress identity = new BitmessageAddress(IDENTITY_A);
assertEquals(4, identity.getVersion()); assertEquals(4, identity.getVersion());
assertEquals(identity, repo.findIdentity(identity.getTag()));
assertNull(repo.findContact(identity.getTag())); assertNull(repo.findContact(identity.getTag()));
BitmessageAddress storedIdentity = repo.findIdentity(identity.getTag());
assertEquals(identity, storedIdentity);
assertTrue(storedIdentity.has(Pubkey.Feature.DOES_ACK));
} }
@Test @Test

View File

@ -19,12 +19,15 @@ package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.UnixTime;
import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -32,8 +35,10 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class JdbcMessageRepositoryTest extends TestBase { public class JdbcMessageRepositoryTest extends TestBase {
@ -54,19 +59,19 @@ public class JdbcMessageRepositoryTest extends TestBase {
AddressRepository addressRepo = new JdbcAddressRepository(config); AddressRepository addressRepo = new JdbcAddressRepository(config);
repo = new JdbcMessageRepository(config); repo = new JdbcMessageRepository(config);
new InternalContext(new BitmessageContext.Builder() new InternalContext(new BitmessageContext.Builder()
.cryptography(security()) .cryptography(cryptography())
.addressRepo(addressRepo) .addressRepo(addressRepo)
.messageRepo(repo) .messageRepo(repo)
); );
BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK));
contactA = new BitmessageAddress(tmp.getAddress()); contactA = new BitmessageAddress(tmp.getAddress());
contactA.setPubkey(tmp.getPubkey()); contactA.setPubkey(tmp.getPubkey());
addressRepo.save(contactA); addressRepo.save(contactA);
contactB = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj"); contactB = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj");
addressRepo.save(contactB); addressRepo.save(contactB);
identity = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); identity = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK));
addressRepo.save(identity); addressRepo.save(identity);
inbox = repo.getLabels(Label.Type.INBOX).get(0); inbox = repo.getLabels(Label.Type.INBOX).get(0);
@ -123,6 +128,18 @@ public class JdbcMessageRepositoryTest extends TestBase {
assertThat(other, is(message)); assertThat(other, is(message));
} }
@Test
public void ensureAckMessageCanBeUpdatedAndRetrieved() {
byte[] initialHash = new byte[64];
Plaintext message = repo.findMessages(contactA).get(0);
message.setInitialHash(initialHash);
ObjectMessage ackMessage = message.getAckMessage();
repo.save(message);
Plaintext other = repo.getMessage(initialHash);
assertThat(other, is(message));
assertThat(other.getAckMessage(), is(ackMessage));
}
@Test @Test
public void testFindMessagesByStatus() throws Exception { public void testFindMessagesByStatus() throws Exception {
List<Plaintext> messages = repo.findMessages(Plaintext.Status.RECEIVED); List<Plaintext> messages = repo.findMessages(Plaintext.Status.RECEIVED);
@ -146,7 +163,7 @@ public class JdbcMessageRepositoryTest extends TestBase {
@Test @Test
public void testSave() throws Exception { public void testSave() throws Exception {
Plaintext message = new Plaintext.Builder(MSG) Plaintext message = new Plaintext.Builder(MSG)
.IV(new InventoryVector(security().randomBytes(32))) .IV(new InventoryVector(cryptography().randomBytes(32)))
.from(identity) .from(identity)
.to(contactA) .to(contactA)
.message("Subject", "Message") .message("Subject", "Message")
@ -169,7 +186,7 @@ public class JdbcMessageRepositoryTest extends TestBase {
public void testUpdate() throws Exception { public void testUpdate() throws Exception {
List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT, contactA);
Plaintext message = messages.get(0); Plaintext message = messages.get(0);
message.setInventoryVector(new InventoryVector(security().randomBytes(32))); message.setInventoryVector(new InventoryVector(cryptography().randomBytes(32)));
repo.save(message); repo.save(message);
messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); messages = repo.findMessages(Plaintext.Status.DRAFT, contactA);
@ -178,11 +195,40 @@ public class JdbcMessageRepositoryTest extends TestBase {
} }
@Test @Test
public void testRemove() throws Exception { public void ensureMessageIsRemoved() throws Exception {
Plaintext toRemove = repo.findMessages(Plaintext.Status.DRAFT, contactB).get(0); Plaintext toRemove = repo.findMessages(Plaintext.Status.DRAFT, contactB).get(0);
repo.remove(toRemove);
List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT); List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT);
assertEquals(1, messages.size()); assertEquals(2, messages.size());
repo.remove(toRemove);
messages = repo.findMessages(Plaintext.Status.DRAFT);
assertThat(messages, hasSize(1));
}
@Test
public void ensureUnacknowledgedMessagesAreFoundForResend() throws Exception {
Plaintext message = new Plaintext.Builder(MSG)
.IV(new InventoryVector(cryptography().randomBytes(32)))
.from(identity)
.to(contactA)
.message("Subject", "Message")
.status(Plaintext.Status.SENT)
.ttl(1)
.build();
message.updateNextTry();
assertThat(message.getRetries(), is(1));
assertThat(message.getNextTry(), greaterThan(UnixTime.now()));
assertThat(message.getNextTry(), lessThanOrEqualTo(UnixTime.now(+1)));
repo.save(message);
Thread.sleep(2100);
List<Plaintext> messagesToResend = repo.findMessagesToResend();
assertThat(messagesToResend, hasSize(1));
message.updateNextTry();
assertThat(message.getRetries(), is(2));
assertThat(message.getNextTry(), greaterThan(UnixTime.now()));
repo.save(message);
messagesToResend = repo.findMessagesToResend();
assertThat(messagesToResend, empty());
} }
private void addMessage(BitmessageAddress from, BitmessageAddress to, Plaintext.Status status, Label... labels) { private void addMessage(BitmessageAddress from, BitmessageAddress to, Plaintext.Status status, Label... labels) {

View File

@ -16,13 +16,24 @@
package ch.dissem.bitmessage.repository; package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.GenericPayload;
import ch.dissem.bitmessage.entity.payload.GetPubkey; import ch.dissem.bitmessage.entity.payload.GetPubkey;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item;
import ch.dissem.bitmessage.utils.TestUtils;
import ch.dissem.bitmessage.utils.UnixTime;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -33,17 +44,51 @@ import static org.junit.Assert.assertTrue;
public class JdbcProofOfWorkRepositoryTest extends TestBase { public class JdbcProofOfWorkRepositoryTest extends TestBase {
private TestJdbcConfig config; private TestJdbcConfig config;
private JdbcProofOfWorkRepository repo; private JdbcProofOfWorkRepository repo;
private AddressRepository addressRepo;
private MessageRepository messageRepo;
private byte[] initialHash1;
private byte[] initialHash2;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
config = new TestJdbcConfig(); config = new TestJdbcConfig();
config.reset(); config.reset();
addressRepo = new JdbcAddressRepository(config);
messageRepo = new JdbcMessageRepository(config);
repo = new JdbcProofOfWorkRepository(config); repo = new JdbcProofOfWorkRepository(config);
InternalContext ctx = new InternalContext(new BitmessageContext.Builder()
.addressRepo(addressRepo)
.messageRepo(messageRepo)
.powRepo(repo)
.cryptography(cryptography())
);
repo.putObject(new ObjectMessage.Builder() repo.putObject(new ObjectMessage.Builder()
.payload(new GetPubkey(new BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(), .payload(new GetPubkey(new BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(),
1000, 1000); 1000, 1000);
initialHash1 = repo.getItems().get(0);
BitmessageAddress sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress recipient = TestUtils.loadContact();
addressRepo.save(sender);
addressRepo.save(recipient);
Plaintext plaintext = new Plaintext.Builder(MSG)
.ackData(cryptography().randomBytes(32))
.from(sender)
.to(recipient)
.message("Subject", "Message")
.status(Plaintext.Status.DOING_PROOF_OF_WORK)
.build();
messageRepo.save(plaintext);
initialHash2 = cryptography().getInitialHash(plaintext.getAckMessage());
repo.putObject(new Item(
plaintext.getAckMessage(),
1000, 1000,
UnixTime.now(+10 * MINUTE),
plaintext
));
} }
@Test @Test
@ -55,16 +100,52 @@ public class JdbcProofOfWorkRepositoryTest extends TestBase {
assertThat(repo.getItems().size(), is(sizeBefore + 1)); assertThat(repo.getItems().size(), is(sizeBefore + 1));
} }
@Test
public void ensureAckObjectsAreStored() throws Exception {
int sizeBefore = repo.getItems().size();
BitmessageAddress sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress recipient = TestUtils.loadContact();
addressRepo.save(sender);
addressRepo.save(recipient);
Plaintext plaintext = new Plaintext.Builder(MSG)
.ackData(cryptography().randomBytes(32))
.from(sender)
.to(recipient)
.message("Subject", "Message")
.status(Plaintext.Status.DOING_PROOF_OF_WORK)
.build();
messageRepo.save(plaintext);
repo.putObject(new Item(
plaintext.getAckMessage(),
1000, 1000,
UnixTime.now(+10 * MINUTE),
plaintext
));
assertThat(repo.getItems().size(), is(sizeBefore + 1));
}
@Test @Test
public void ensureItemCanBeRetrieved() { public void ensureItemCanBeRetrieved() {
byte[] initialHash = repo.getItems().get(0); Item item = repo.getItem(initialHash1);
ProofOfWorkRepository.Item item = repo.getItem(initialHash);
assertThat(item, notNullValue()); assertThat(item, notNullValue());
assertThat(item.object.getPayload(), instanceOf(GetPubkey.class)); assertThat(item.object.getPayload(), instanceOf(GetPubkey.class));
assertThat(item.nonceTrialsPerByte, is(1000L)); assertThat(item.nonceTrialsPerByte, is(1000L));
assertThat(item.extraBytes, is(1000L)); assertThat(item.extraBytes, is(1000L));
} }
@Test
public void ensureAckItemCanBeRetrieved() {
Item item = repo.getItem(initialHash2);
assertThat(item, notNullValue());
assertThat(item.object.getPayload(), instanceOf(GenericPayload.class));
assertThat(item.nonceTrialsPerByte, is(1000L));
assertThat(item.extraBytes, is(1000L));
assertThat(item.expirationTime, not(0));
assertThat(item.message, notNullValue());
assertThat(item.message.getFrom().getPrivateKey(), notNullValue());
assertThat(item.message.getTo().getPubkey(), notNullValue());
}
@Test(expected = RuntimeException.class) @Test(expected = RuntimeException.class)
public void ensureRetrievingNonexistingItemThrowsException() { public void ensureRetrievingNonexistingItemThrowsException() {
repo.getItem(new byte[0]); repo.getItem(new byte[0]);
@ -72,8 +153,8 @@ public class JdbcProofOfWorkRepositoryTest extends TestBase {
@Test @Test
public void ensureItemCanBeDeleted() { public void ensureItemCanBeDeleted() {
byte[] initialHash = repo.getItems().get(0); repo.removeObject(initialHash1);
repo.removeObject(initialHash); repo.removeObject(initialHash2);
assertTrue(repo.getItems().isEmpty()); assertTrue(repo.getItems().isEmpty());
} }

View File

@ -27,7 +27,7 @@ import java.io.*;
import java.util.Collection; import java.util.Collection;
import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* @author Christian Basler * @author Christian Basler
@ -77,7 +77,7 @@ public class WifExporter {
byte[] result = new byte[37]; byte[] result = new byte[37];
result[0] = (byte) 0x80; result[0] = (byte) 0x80;
System.arraycopy(privateKey, 0, result, 1, PRIVATE_KEY_SIZE); System.arraycopy(privateKey, 0, result, 1, PRIVATE_KEY_SIZE);
byte[] hash = security().doubleSha256(result, PRIVATE_KEY_SIZE + 1); byte[] hash = cryptography().doubleSha256(result, PRIVATE_KEY_SIZE + 1);
System.arraycopy(hash, 0, result, PRIVATE_KEY_SIZE + 1, 4); System.arraycopy(hash, 0, result, PRIVATE_KEY_SIZE + 1, 4);
return Base58.encode(result); return Base58.encode(result);
} }

View File

@ -31,7 +31,7 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
* @author Christian Basler * @author Christian Basler
@ -87,7 +87,7 @@ public class WifImporter {
throw new IOException("Unknown format: " + WIF_SECRET_LENGTH + throw new IOException("Unknown format: " + WIF_SECRET_LENGTH +
" bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); " bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long");
byte[] hash = security().doubleSha256(bytes, 33); byte[] hash = cryptography().doubleSha256(bytes, 33);
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat); if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat);
} }