Merge branch 'develop' into feature/ACK

# Conflicts:
#	core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java
#	core/src/main/java/ch/dissem/bitmessage/InternalContext.java
#	core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java
#	core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java
This commit is contained in:
Christian Basler 2016-04-11 23:41:23 +02:00
commit 6f4f51aef3
157 changed files with 5301 additions and 1475 deletions

View File

@ -1,3 +1,10 @@
language: java language: java
sudo: false # faster builds
jdk: jdk:
- oraclejdk8 - oraclejdk8
before_install:
- pip install --user codecov
after_success:
- codecov

File diff suppressed because it is too large Load Diff

24
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,24 @@
# Contributing
We love pull requests from everyone. Please be nice and forgive us
if we can't process your request right away.
Fork, then clone the repo:
git clone git@github.com:your-username/Jabit.git
Make sure the tests pass:
./gradlew test
Make your change. Add tests for your change. Make the tests pass:
./gradlew test
Push to your fork and [submit a pull request][pr].
[pr]: https://github.com/Dissem/Jabit/compare/
Unfortunately we can't always answer right away, so we ask you to have
some patience. Then we may suggest some changes or improvements or
alternatives.

View File

@ -1,21 +1,38 @@
Jabit [![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) Jabit
===== =====
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core)
[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.jabit/jabit-core/badge.svg)](http://www.javadoc.io/doc/ch.dissem.jabit/jabit-core)
[![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)
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 `gradle build` or `./gradlew build`.
Please note that development is still heavily in progress, and I will break the database a lot until it's ready for prime time. 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.
#### Master
[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit)
[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/master.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144281)
[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=master)](https://codecov.io/github/Dissem/Jabit?branch=master)
#### Develop
[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=develop)](https://travis-ci.org/Dissem/Jabit?branch=develop)
[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/develop.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144279)
[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=develop)](https://codecov.io/github/Dissem/Jabit?branch=develop)
Security Security
-------- --------
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. 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.
Project Status Project Status
-------------- --------------
Basically, everything needed for a working Bitmessage client is there: Basically, everything needed for a working Bitmessage client is there:
* Creating new identities (private addresses) * Creating new identities (private addresses)
* Adding contracts and subscriptions * Adding contacts and subscriptions
* Receiving broadcasts * Receiving broadcasts
* Receiving messages * Receiving messages
* Sending messages and broadcasts * Sending messages and broadcasts
@ -29,18 +46,21 @@ Setup
Add Jabit as Gradle dependency: Add Jabit as Gradle dependency:
```Gradle ```Gradle
compile 'ch.dissem.jabit:jabit-domain:0.2.0' compile 'ch.dissem.jabit:jabit-core:1.0.0'
``` ```
Unless you want to implement your own, also add the following: Unless you want to implement your own, also add the following:
```Gradle ```Gradle
compile 'ch.dissem.jabit:jabit-networking:0.2.0' compile 'ch.dissem.jabit:jabit-networking:1.0.0'
compile 'ch.dissem.jabit:jabit-repositories:0.2.0' compile 'ch.dissem.jabit:jabit-repositories:1.0.0'
compile 'ch.dissem.jabit:jabit-cryptography-bouncy:1.0.0'
``` ```
And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add: And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add:
```Gradle ```Gradle
compile 'ch.dissem.jabit:jabit-wif:0.2.0' compile 'ch.dissem.jabit:jabit-wif:1.0.0'
``` ```
For Android clients use `jabit-cryptography-spongy` instead of `jabit-cryptography-bouncy`.
Usage Usage
----- -----
@ -53,6 +73,7 @@ BitmessageContext ctx = new BitmessageContext.Builder()
.messageRepo(new JdbcMessageRepository(jdbcConfig)) .messageRepo(new JdbcMessageRepository(jdbcConfig))
.nodeRegistry(new MemoryNodeRegistry()) .nodeRegistry(new MemoryNodeRegistry())
.networkHandler(new NetworkNode()) .networkHandler(new NetworkNode())
.cryptography(new BouncyCryptography())
.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. Next you'll need to

View File

@ -2,10 +2,11 @@ subprojects {
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'maven' apply plugin: 'maven'
apply plugin: 'signing' apply plugin: 'signing'
apply plugin: 'jacoco'
sourceCompatibility = 1.7 sourceCompatibility = 1.7
group = 'ch.dissem.jabit' group = 'ch.dissem.jabit'
version = '0.2.1-SNAPSHOT' version = '1.1.0-SNAPSHOT'
ext.isReleaseVersion = !version.endsWith("SNAPSHOT") ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
@ -34,7 +35,7 @@ subprojects {
} }
signing { signing {
required { isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives") } required { isReleaseVersion && project.getProperties().get("signing.keyId")?.length() > 0 }
sign configurations.archives sign configurations.archives
} }
@ -79,4 +80,13 @@ subprojects {
} }
} }
} }
jacocoTestReport {
reports {
xml.enabled = true
html.enabled = true
}
}
check.dependsOn jacocoTestReport
} }

View File

@ -2,8 +2,8 @@ uploadArchives {
repositories { repositories {
mavenDeployer { mavenDeployer {
pom.project { pom.project {
name 'Jabit Domain' name 'Jabit Core'
artifactId = 'jabit-domain' artifactId = 'jabit-core'
description 'A Java implementation of the Bitmessage protocol. This is the core part. You\'ll either need the networking and repositories modules, too, or implement your own.' description 'A Java implementation of the Bitmessage protocol. This is the core part. You\'ll either need the networking and repositories modules, too, or implement your own.'
} }
} }
@ -27,5 +27,5 @@ 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.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'
testCompile project(':security-bc') testCompile project(':cryptography-bc')
} }

View File

@ -0,0 +1,47 @@
/*
* 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

@ -16,30 +16,35 @@
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.Broadcast;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.payload.ObjectPayload;
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.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.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.*;
import ch.dissem.bitmessage.utils.Property; import ch.dissem.bitmessage.utils.Property;
import ch.dissem.bitmessage.utils.TTL;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Arrays; import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*; import java.util.concurrent.*;
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.entity.Plaintext.Status.*; 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.DAY; import static ch.dissem.bitmessage.utils.UnixTime.*;
import static ch.dissem.bitmessage.utils.UnixTime.HOUR;
/** /**
* <p>Use this class if you want to create a Bitmessage client.</p> * <p>Use this class if you want to create a Bitmessage client.</p>
@ -63,123 +68,155 @@ public class BitmessageContext {
private final InternalContext ctx; private final InternalContext ctx;
private final Labeler labeler;
private final Listener listener; private final Listener listener;
private final NetworkHandler.MessageListener networkListener; private final NetworkHandler.MessageListener networkListener;
private final boolean sendPubkeyOnIdentityCreation;
private BitmessageContext(Builder builder) { private BitmessageContext(Builder builder) {
ctx = new InternalContext(builder); ctx = new InternalContext(builder);
labeler = builder.labeler;
listener = builder.listener; listener = builder.listener;
networkListener = new DefaultMessageListener(ctx, listener); networkListener = new DefaultMessageListener(ctx, labeler, listener);
// As this thread is used for parts that do POW, which itself uses parallel threads, only // As this thread is used for parts that do POW, which itself uses parallel threads, only
// one should be executed at any time. // one should be executed at any time.
pool = Executors.newFixedThreadPool(1); pool = Executors.newFixedThreadPool(1);
sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
ctx.getProofOfWorkService().doMissingProofOfWork();
}
}, 30_000); // After 30 seconds
} }
public AddressRepository addresses() { public AddressRepository addresses() {
return ctx.getAddressRepo(); return ctx.getAddressRepository();
} }
public MessageRepository messages() { public MessageRepository messages() {
return ctx.getMessageRepository(); return ctx.getMessageRepository();
} }
public Labeler labeler() {
return labeler;
}
public BitmessageAddress createIdentity(boolean shorter, Feature... features) { public BitmessageAddress createIdentity(boolean shorter, Feature... features) {
final BitmessageAddress identity = new BitmessageAddress(new PrivateKey( final BitmessageAddress identity = new BitmessageAddress(new PrivateKey(
shorter, shorter,
ctx.getStreams()[0], ctx.getStreams()[0],
ctx.getNetworkNonceTrialsPerByte(), NETWORK_NONCE_TRIALS_PER_BYTE,
ctx.getNetworkExtraBytes(), NETWORK_EXTRA_BYTES,
features features
)); ));
ctx.getAddressRepo().save(identity); ctx.getAddressRepository().save(identity);
if (sendPubkeyOnIdentityCreation) {
pool.submit(new Runnable() { pool.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
ctx.sendPubkey(identity, identity.getStream()); ctx.sendPubkey(identity, identity.getStream());
} }
}); });
}
return identity; return identity;
} }
public void addDistributedMailingList(String address, String alias) { public BitmessageAddress joinChan(String passphrase, String address) {
// TODO BitmessageAddress chan = BitmessageAddress.chan(address, passphrase);
chan.setAlias(passphrase);
ctx.getAddressRepository().save(chan);
return chan;
}
public BitmessageAddress createChan(String passphrase) {
// FIXME: hardcoded stream number
BitmessageAddress chan = BitmessageAddress.chan(1, passphrase);
ctx.getAddressRepository().save(chan);
return chan;
}
public List<BitmessageAddress> createDeterministicAddresses(
String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) {
List<BitmessageAddress> result = BitmessageAddress.deterministic(
passphrase, numberOfAddresses, version, stream, shorter);
for (int i = 0; i < result.size(); i++) {
BitmessageAddress address = result.get(i);
address.setAlias("deterministic (" + (i + 1) + ")");
ctx.getAddressRepository().save(address);
}
return result;
} }
public void broadcast(final BitmessageAddress from, final String subject, final String message) { public void broadcast(final BitmessageAddress from, final String subject, final String message) {
pool.submit(new Runnable() {
@Override
public void run() {
Plaintext msg = new Plaintext.Builder(BROADCAST) Plaintext msg = new Plaintext.Builder(BROADCAST)
.from(from) .from(from)
.message(subject, message) .message(subject, message)
.build(); .build();
send(msg);
LOG.info("Sending message.");
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
from,
from,
Factory.getBroadcast(from, msg),
+2 * DAY,
0,
0
);
msg.setStatus(SENT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.BROADCAST, Label.Type.SENT));
ctx.getMessageRepository().save(msg);
}
});
} }
public void send(final BitmessageAddress from, final BitmessageAddress to, final String subject, final String message) { public void send(final BitmessageAddress from, final BitmessageAddress to, final String subject, final String message) {
if (from.getPrivateKey() == null) { if (from.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.");
} }
pool.submit(new Runnable() {
@Override
public void run() {
Plaintext msg = new Plaintext.Builder(MSG) Plaintext msg = new Plaintext.Builder(MSG)
.from(from) .from(from)
.to(to) .to(to)
.message(subject, message) .message(subject, message)
.labels(messages().getLabels(Label.Type.SENT))
.build(); .build();
if (to.getPubkey() == null) { send(msg);
tryToFindMatchingPubkey(to);
} }
public void send(final Plaintext msg) {
if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) {
throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key.");
}
pool.submit(new Runnable() {
@Override
public void run() {
BitmessageAddress to = msg.getTo();
if (to != null) {
if (to.getPubkey() == null) { if (to.getPubkey() == null) {
LOG.info("Public key is missing from recipient. Requesting."); LOG.info("Public key is missing from recipient. Requesting.");
requestPubkey(from, to); ctx.requestPubkey(to);
}
if (to.getPubkey() == null) {
msg.setStatus(PUBKEY_REQUESTED); msg.setStatus(PUBKEY_REQUESTED);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX));
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
} else { }
}
if (to == null || to.getPubkey() != null) {
LOG.info("Sending message."); LOG.info("Sending message.");
msg.setStatus(DOING_PROOF_OF_WORK); msg.setStatus(DOING_PROOF_OF_WORK);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX)); msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX));
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
ctx.send( ctx.send(
from, msg.getFrom(),
to, to,
new Msg(msg), wrapInObjectPayload(msg),
+2 * DAY, TTL.msg()
ctx.getNonceTrialsPerByte(to),
ctx.getExtraBytes(to)
); );
} }
} }
}); });
} }
private void requestPubkey(BitmessageAddress requestingIdentity, BitmessageAddress address) { private ObjectPayload wrapInObjectPayload(Plaintext msg) {
ctx.send( switch (msg.getType()) {
requestingIdentity, case MSG:
address, return new Msg(msg);
new GetPubkey(address), case BROADCAST:
+28 * DAY, return Factory.getBroadcast(msg);
ctx.getNetworkNonceTrialsPerByte(), default:
ctx.getNetworkExtraBytes() throw new ApplicationException("Unknown message type " + msg.getType());
); }
} }
public void startup() { public void startup() {
@ -211,6 +248,19 @@ public class BitmessageContext {
} }
} }
/**
* Send a custom message to a specific node (that should implement handling for this message type) and returns
* the response, which in turn is expected to be a {@link CustomMessage}.
*
* @param server the node's address
* @param port the node's port
* @param request the request
* @return the response
*/
public CustomMessage send(InetAddress server, int port, CustomMessage request) {
return ctx.getNetworkHandler().send(server, port, request);
}
public void cleanup() { public void cleanup() {
ctx.getInventory().cleanup(); ctx.getInventory().cleanup();
} }
@ -220,45 +270,15 @@ public class BitmessageContext {
} }
public void addContact(BitmessageAddress contact) { public void addContact(BitmessageAddress contact) {
ctx.getAddressRepo().save(contact); ctx.getAddressRepository().save(contact);
tryToFindMatchingPubkey(contact);
if (contact.getPubkey() == null) { if (contact.getPubkey() == null) {
ctx.requestPubkey(contact); ctx.requestPubkey(contact);
} }
} }
private void tryToFindMatchingPubkey(BitmessageAddress address) {
for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) {
try {
Pubkey pubkey = (Pubkey) object.getPayload();
if (address.getVersion() == 4) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
if (object.isSignatureValid(v4Pubkey)) {
address.setPubkey(v4Pubkey);
ctx.getAddressRepo().save(address);
break;
} else {
LOG.info("Found pubkey for " + address + " but signature is invalid");
}
}
} else {
if (Arrays.equals(pubkey.getRipe(), address.getRipe())) {
address.setPubkey(pubkey);
ctx.getAddressRepo().save(address);
break;
}
}
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
}
}
public void addSubscribtion(BitmessageAddress address) { public void addSubscribtion(BitmessageAddress address) {
address.setSubscribed(true); address.setSubscribed(true);
ctx.getAddressRepo().save(address); ctx.getAddressRepository().save(address);
tryToFindBroadcastsForAddress(address); tryToFindBroadcastsForAddress(address);
} }
@ -267,7 +287,9 @@ public class BitmessageContext {
try { try {
Broadcast broadcast = (Broadcast) object.getPayload(); Broadcast broadcast = (Broadcast) object.getPayload();
broadcast.decrypt(address); broadcast.decrypt(address);
listener.receive(broadcast.getPlaintext()); // This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with
// other subscriptions and the interface stays as simple as possible.
networkListener.receive(object);
} catch (DecryptionFailedException ignore) { } catch (DecryptionFailedException ignore) {
} catch (Exception e) { } catch (Exception e) {
LOG.debug(e.getMessage(), e); LOG.debug(e.getMessage(), e);
@ -281,6 +303,14 @@ public class BitmessageContext {
); );
} }
/**
* Returns the {@link InternalContext} - normally you wouldn't need it,
* unless you are doing something crazy with the protocol.
*/
public InternalContext internals() {
return ctx;
}
public interface Listener { public interface Listener {
void receive(Plaintext plaintext); void receive(Plaintext plaintext);
} }
@ -292,15 +322,16 @@ public class BitmessageContext {
NetworkHandler networkHandler; NetworkHandler networkHandler;
AddressRepository addressRepo; AddressRepository addressRepo;
MessageRepository messageRepo; MessageRepository messageRepo;
ProofOfWorkRepository proofOfWorkRepository;
ProofOfWorkEngine proofOfWorkEngine; ProofOfWorkEngine proofOfWorkEngine;
Security security; Cryptography cryptography;
MessageCallback messageCallback; MessageCallback messageCallback;
CustomCommandHandler customCommandHandler;
Labeler labeler;
Listener listener; Listener listener;
int connectionLimit = 150; int connectionLimit = 150;
long connectionTTL = 12 * HOUR; long connectionTTL = 30 * MINUTE;
boolean sendPubkeyOnIdentityCreation = true;
public Builder() {
}
public Builder port(int port) { public Builder port(int port) {
this.port = port; this.port = port;
@ -332,8 +363,13 @@ public class BitmessageContext {
return this; return this;
} }
public Builder security(Security security) { public Builder powRepo(ProofOfWorkRepository proofOfWorkRepository) {
this.security = security; this.proofOfWorkRepository = proofOfWorkRepository;
return this;
}
public Builder cryptography(Cryptography cryptography) {
this.cryptography = cryptography;
return this; return this;
} }
@ -342,11 +378,21 @@ public class BitmessageContext {
return this; return this;
} }
public Builder customCommandHandler(CustomCommandHandler handler) {
this.customCommandHandler = handler;
return this;
}
public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) { public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) {
this.proofOfWorkEngine = proofOfWorkEngine; this.proofOfWorkEngine = proofOfWorkEngine;
return this; return this;
} }
public Builder labeler(Labeler labeler) {
this.labeler = labeler;
return this;
}
public Builder listener(Listener listener) { public Builder listener(Listener listener) {
this.listener = listener; this.listener = listener;
return this; return this;
@ -362,31 +408,52 @@ public class BitmessageContext {
return this; return this;
} }
/**
* By default a client will send the public key when an identity is being created. On weaker devices
* this behaviour might not be desirable.
*/
public Builder doNotSendPubkeyOnIdentityCreation() {
this.sendPubkeyOnIdentityCreation = false;
return this;
}
/**
* Time to live in seconds for public keys the client sends. Defaults to the maximum of 28 days,
* but on weak devices smaller values might be desirable.
* <p>
* Please be aware that this might cause some problems where you can't receive a message (the
* sender can't receive your public key) in some special situations. Also note that it's probably
* not a good idea to set it too low.
* </p>
*/
public Builder pubkeyTTL(long days) {
if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days");
TTL.pubkey(days);
return this;
}
public BitmessageContext build() { public BitmessageContext build() {
nonNull("inventory", inventory); nonNull("inventory", inventory);
nonNull("nodeRegistry", nodeRegistry); nonNull("nodeRegistry", nodeRegistry);
nonNull("networkHandler", networkHandler); nonNull("networkHandler", networkHandler);
nonNull("addressRepo", addressRepo); nonNull("addressRepo", addressRepo);
nonNull("messageRepo", messageRepo); nonNull("messageRepo", messageRepo);
nonNull("proofOfWorkRepo", proofOfWorkRepository);
if (proofOfWorkEngine == null) { if (proofOfWorkEngine == null) {
proofOfWorkEngine = new MultiThreadedPOWEngine(); proofOfWorkEngine = new MultiThreadedPOWEngine();
} }
if (messageCallback == null) { if (messageCallback == null) {
messageCallback = new MessageCallback() { messageCallback = new BaseMessageCallback();
@Override
public void proofOfWorkStarted(ObjectPayload message) {
} }
if (labeler == null) {
@Override labeler = new DefaultLabeler();
public void proofOfWorkCompleted(ObjectPayload message) {
} }
if (customCommandHandler == null) {
customCommandHandler = new CustomCommandHandler() {
@Override @Override
public void messageOffered(ObjectPayload message, InventoryVector iv) { public MessagePayload handle(CustomMessage request) {
} throw new IllegalStateException(
"Received custom request, but no custom command handler configured.");
@Override
public void messageAcknowledged(InventoryVector iv) {
} }
}; };
} }

View File

@ -20,8 +20,9 @@ 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.payload.*; import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.Label; 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.NetworkHandler; import ch.dissem.bitmessage.ports.NetworkHandler;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -36,10 +37,12 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY;
class DefaultMessageListener implements NetworkHandler.MessageListener { class DefaultMessageListener implements NetworkHandler.MessageListener {
private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class); private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class);
private final InternalContext ctx; private final InternalContext ctx;
private final Labeler labeler;
private final BitmessageContext.Listener listener; private final BitmessageContext.Listener listener;
public DefaultMessageListener(InternalContext context, BitmessageContext.Listener listener) { public DefaultMessageListener(InternalContext context, Labeler labeler, BitmessageContext.Listener listener) {
this.ctx = context; this.ctx = context;
this.labeler = labeler;
this.listener = listener; this.listener = listener;
} }
@ -65,12 +68,15 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
receive(object, (Broadcast) payload); receive(object, (Broadcast) payload);
break; break;
} }
default: {
throw new IllegalArgumentException("Unknown payload type " + payload.getType());
}
} }
} }
protected void receive(ObjectMessage object, GetPubkey getPubkey) { protected void receive(ObjectMessage object, GetPubkey getPubkey) {
BitmessageAddress identity = ctx.getAddressRepo().findIdentity(getPubkey.getRipeTag()); BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag());
if (identity != null && identity.getPrivateKey() != null) { if (identity != null && identity.getPrivateKey() != null && !identity.isChan()) {
LOG.info("Got pubkey request for identity " + identity); LOG.info("Got pubkey request for identity " + identity);
// FIXME: only send pubkey if it wasn't sent in the last 28 days // FIXME: only send pubkey if it wasn't sent in the last 28 days
ctx.sendPubkey(identity, object.getStream()); ctx.sendPubkey(identity, object.getStream());
@ -82,18 +88,25 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
try { try {
if (pubkey instanceof V4Pubkey) { if (pubkey instanceof V4Pubkey) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey; V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
address = ctx.getAddressRepo().findContact(v4Pubkey.getTag()); address = ctx.getAddressRepository().findContact(v4Pubkey.getTag());
if (address != null) { if (address != null) {
v4Pubkey.decrypt(address.getPublicDecryptionKey()); v4Pubkey.decrypt(address.getPublicDecryptionKey());
} }
} else { } else {
address = ctx.getAddressRepo().findContact(pubkey.getRipe()); address = ctx.getAddressRepository().findContact(pubkey.getRipe());
} }
if (address != null) { if (address != null) {
updatePubkey(address, pubkey);
}
} catch (DecryptionFailedException ignore) {
}
}
private void updatePubkey(BitmessageAddress address, Pubkey pubkey) {
address.setPubkey(pubkey); address.setPubkey(pubkey);
LOG.info("Got pubkey for contact " + address); LOG.info("Got pubkey for contact " + address);
ctx.getAddressRepo().save(address); ctx.getAddressRepository().save(address);
List<Plaintext> messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address); 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); msg.setStatus(DOING_PROOF_OF_WORK);
@ -102,20 +115,15 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
msg.getFrom(), msg.getFrom(),
msg.getTo(), msg.getTo(),
new Msg(msg), new Msg(msg),
+2 * DAY, +2 * DAY
ctx.getNonceTrialsPerByte(msg.getTo()),
ctx.getExtraBytes(msg.getTo())
); );
msg.setStatus(SENT); msg.setStatus(SENT);
ctx.getMessageRepository().save(msg); ctx.getMessageRepository().save(msg);
} }
} }
} catch (DecryptionFailedException ignore) {
}
}
protected void receive(ObjectMessage object, Msg msg) throws IOException { protected void receive(ObjectMessage object, Msg msg) throws IOException {
for (BitmessageAddress identity : ctx.getAddressRepo().getIdentities()) { for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) {
try { try {
msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey()); msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey());
Plaintext plaintext = msg.getPlaintext(); Plaintext plaintext = msg.getPlaintext();
@ -123,19 +131,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
if (!object.isSignatureValid(plaintext.getFrom().getPubkey())) { 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 {
plaintext.setStatus(RECEIVED); receive(object.getInventoryVector(), msg.getPlaintext());
plaintext.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD));
plaintext.setInventoryVector(object.getInventoryVector());
ctx.getMessageRepository().save(plaintext);
listener.receive(plaintext);
if (identity.has(Pubkey.Feature.DOES_ACK)) {
ObjectMessage ack = plaintext.getAckMessage();
if (ack != null) {
ctx.getInventory().storeObject(ack);
ctx.getNetworkHandler().offer(ack.getInventoryVector());
}
}
} }
break; break;
} catch (DecryptionFailedException ignore) { } catch (DecryptionFailedException ignore) {
@ -145,7 +141,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
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.getAddressRepo().getSubscriptions(broadcast.getVersion())) { for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) {
if (tag != null && !Arrays.equals(tag, subscription.getTag())) { if (tag != null && !Arrays.equals(tag, subscription.getTag())) {
continue; continue;
} }
@ -154,14 +150,27 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) { if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) {
LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
} else { } else {
broadcast.getPlaintext().setStatus(RECEIVED); receive(object.getInventoryVector(), broadcast.getPlaintext());
broadcast.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
broadcast.getPlaintext().setInventoryVector(object.getInventoryVector());
ctx.getMessageRepository().save(broadcast.getPlaintext());
listener.receive(broadcast.getPlaintext());
} }
} catch (DecryptionFailedException ignore) { } catch (DecryptionFailedException ignore) {
} }
} }
} }
protected void receive(InventoryVector iv, Plaintext msg) {
msg.setStatus(RECEIVED);
msg.setInventoryVector(iv);
labeler.setLabels(msg);
ctx.getMessageRepository().save(msg);
listener.receive(msg);
updatePubkey(msg.getFrom(), msg.getFrom().getPubkey());
if (msg.getType() == Type.MSG && identity.has(Pubkey.Feature.DOES_ACK)) {
ObjectMessage ack = plaintext.getAckMessage();
if (ack != null) {
ctx.getInventory().storeObject(ack);
ctx.getNetworkHandler().offer(ack.getInventoryVector());
}
}
}
} }

View File

@ -16,16 +16,23 @@
package ch.dissem.bitmessage; package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.entity.*; import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.UnixTime; import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.TreeSet; import java.util.TreeSet;
import static ch.dissem.bitmessage.entity.Plaintext.Status.SENT; import static ch.dissem.bitmessage.entity.Plaintext.Status.SENT;
@ -42,38 +49,45 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY;
public class InternalContext { public class InternalContext {
private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class); private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class);
private final Security security; public final static long NETWORK_NONCE_TRIALS_PER_BYTE = 1000;
public final static long NETWORK_EXTRA_BYTES = 1000;
private final Cryptography cryptography;
private final Inventory inventory; private final Inventory inventory;
private final NodeRegistry nodeRegistry; private final NodeRegistry nodeRegistry;
private final NetworkHandler networkHandler; private final NetworkHandler networkHandler;
private final AddressRepository addressRepository; private final AddressRepository addressRepository;
private final MessageRepository messageRepository; private final MessageRepository messageRepository;
private final ProofOfWorkRepository proofOfWorkRepository;
private final ProofOfWorkEngine proofOfWorkEngine; private final ProofOfWorkEngine proofOfWorkEngine;
private final MessageCallback messageCallback; private final MessageCallback messageCallback;
private final CustomCommandHandler customCommandHandler;
private final ProofOfWorkService proofOfWorkService;
private final TreeSet<Long> streams = new TreeSet<>(); private final TreeSet<Long> streams = new TreeSet<>();
private final int port; private final int port;
private final long clientNonce; private final long clientNonce;
private final long networkNonceTrialsPerByte = 1000;
private final long networkExtraBytes = 1000;
private long connectionTTL; private long connectionTTL;
private int connectionLimit; private int connectionLimit;
public InternalContext(BitmessageContext.Builder builder) { public InternalContext(BitmessageContext.Builder builder) {
this.security = builder.security; this.cryptography = builder.cryptography;
this.inventory = builder.inventory; this.inventory = builder.inventory;
this.nodeRegistry = builder.nodeRegistry; this.nodeRegistry = builder.nodeRegistry;
this.networkHandler = builder.networkHandler; this.networkHandler = builder.networkHandler;
this.addressRepository = builder.addressRepo; this.addressRepository = builder.addressRepo;
this.messageRepository = builder.messageRepo; this.messageRepository = builder.messageRepo;
this.proofOfWorkRepository = builder.proofOfWorkRepository;
this.proofOfWorkService = new ProofOfWorkService();
this.proofOfWorkEngine = builder.proofOfWorkEngine; this.proofOfWorkEngine = builder.proofOfWorkEngine;
this.clientNonce = security.randomNonce(); this.clientNonce = cryptography.randomNonce();
this.messageCallback = builder.messageCallback; this.messageCallback = builder.messageCallback;
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;
Singleton.initialize(security); Singleton.initialize(cryptography);
// TODO: streams of new identities and subscriptions should also be added. This works only after a restart. // TODO: streams of new identities and subscriptions should also be added. This works only after a restart.
for (BitmessageAddress address : addressRepository.getIdentities()) { for (BitmessageAddress address : addressRepository.getIdentities()) {
@ -86,7 +100,9 @@ public class InternalContext {
streams.add(1L); streams.add(1L);
} }
init(security, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, proofOfWorkEngine); init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine,
messageCallback, customCommandHandler, builder.labeler);
for (BitmessageAddress identity : addressRepository.getIdentities()) { for (BitmessageAddress identity : addressRepository.getIdentities()) {
streams.add(identity.getStream()); streams.add(identity.getStream());
} }
@ -100,8 +116,8 @@ public class InternalContext {
} }
} }
public Security getSecurity() { public Cryptography getCryptography() {
return security; return cryptography;
} }
public Inventory getInventory() { public Inventory getInventory() {
@ -116,7 +132,7 @@ public class InternalContext {
return networkHandler; return networkHandler;
} }
public AddressRepository getAddressRepo() { public AddressRepository getAddressRepository() {
return addressRepository; return addressRepository;
} }
@ -124,10 +140,18 @@ public class InternalContext {
return messageRepository; return messageRepository;
} }
public ProofOfWorkRepository getProofOfWorkRepository() {
return proofOfWorkRepository;
}
public ProofOfWorkEngine getProofOfWorkEngine() { public ProofOfWorkEngine getProofOfWorkEngine() {
return proofOfWorkEngine; return proofOfWorkEngine;
} }
public ProofOfWorkService getProofOfWorkService() {
return proofOfWorkService;
}
public long[] getStreams() { public long[] getStreams() {
long[] result = new long[streams.size()]; long[] result = new long[streams.size()];
int i = 0; int i = 0;
@ -141,26 +165,8 @@ public class InternalContext {
return port; return port;
} }
public long getNetworkNonceTrialsPerByte() {
return networkNonceTrialsPerByte;
}
public long getNonceTrialsPerByte(BitmessageAddress address) {
long nonceTrialsPerByte = address.getPubkey().getNonceTrialsPerByte();
return networkNonceTrialsPerByte > nonceTrialsPerByte ? networkNonceTrialsPerByte : nonceTrialsPerByte;
}
public long getNetworkExtraBytes() {
return networkExtraBytes;
}
public long getExtraBytes(BitmessageAddress address) {
long extraBytes = address.getPubkey().getExtraBytes();
return networkExtraBytes > extraBytes ? networkExtraBytes : extraBytes;
}
public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload, public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload,
final long timeToLive, final long nonceTrialsPerByte, final long extraBytes) { final long timeToLive) {
try { try {
final BitmessageAddress recipient = (to != null ? to : from); final BitmessageAddress recipient = (to != null ? to : from);
long expires = UnixTime.now(+timeToLive); long expires = UnixTime.now(+timeToLive);
@ -180,7 +186,7 @@ public class InternalContext {
@Override @Override
public void onNonceCalculated(byte[] nonce) { public void onNonceCalculated(byte[] nonce) {
object.encrypt(recipient.getPubkey()); object.encrypt(recipient.getPubkey());
security.doProofOfWork(object, nonceTrialsPerByte, extraBytes, new ProofOfWorkCallback(object, payload)); proofOfWorkService.doProofOfWork(to, object);
} }
}); });
} else { } else {
@ -189,16 +195,16 @@ public class InternalContext {
} else if (payload instanceof Encrypted) { } else if (payload instanceof Encrypted) {
object.encrypt(recipient.getPubkey()); object.encrypt(recipient.getPubkey());
} }
security.doProofOfWork(object, nonceTrialsPerByte, extraBytes, new ProofOfWorkCallback(object, payload)); proofOfWorkService.doProofOfWork(to, object);
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
public void sendPubkey(final BitmessageAddress identity, final long targetStream) { public void sendPubkey(final BitmessageAddress identity, final long targetStream) {
try { try {
long expires = UnixTime.now(+28 * DAY); long expires = UnixTime.now(TTL.pubkey());
LOG.info("Expires at " + expires); LOG.info("Expires at " + expires);
final ObjectMessage response = new ObjectMessage.Builder() final ObjectMessage response = new ObjectMessage.Builder()
.stream(targetStream) .stream(targetStream)
@ -206,45 +212,80 @@ public class InternalContext {
.payload(identity.getPubkey()) .payload(identity.getPubkey())
.build(); .build();
response.sign(identity.getPrivateKey()); response.sign(identity.getPrivateKey());
response.encrypt(security.createPublicKey(identity.getPublicDecryptionKey())); response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey()));
messageCallback.proofOfWorkStarted(identity.getPubkey()); messageCallback.proofOfWorkStarted(identity.getPubkey());
security.doProofOfWork(response, networkNonceTrialsPerByte, networkExtraBytes, // TODO: remember that the pubkey is just about to be sent, and on which stream!
new ProofOfWorkEngine.Callback() { proofOfWorkService.doProofOfWork(response);
@Override
public void onNonceCalculated(byte[] nonce) {
response.setNonce(nonce);
messageCallback.proofOfWorkCompleted(identity.getPubkey());
inventory.storeObject(response);
networkHandler.offer(response.getInventoryVector());
// TODO: save that the pubkey was just sent, and on which stream!
messageCallback.messageOffered(identity.getPubkey(), response.getInventoryVector());
}
});
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
/**
* Be aware that if the pubkey already exists in the inventory, the metods will not request it and the callback
* for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB.
*/
public void requestPubkey(final BitmessageAddress contact) { public void requestPubkey(final BitmessageAddress contact) {
long expires = UnixTime.now(+2 * DAY); BitmessageAddress stored = addressRepository.getAddress(contact.getAddress());
tryToFindMatchingPubkey(contact);
if (contact.getPubkey() != null) {
if (stored != null) {
stored.setPubkey(contact.getPubkey());
addressRepository.save(stored);
} else {
addressRepository.save(contact);
}
return;
}
if (stored == null) {
addressRepository.save(contact);
}
long expires = UnixTime.now(TTL.getpubkey());
LOG.info("Expires at " + expires); LOG.info("Expires at " + expires);
final ObjectMessage response = new ObjectMessage.Builder() final ObjectMessage request = new ObjectMessage.Builder()
.stream(contact.getStream()) .stream(contact.getStream())
.expiresTime(expires) .expiresTime(expires)
.payload(new GetPubkey(contact)) .payload(new GetPubkey(contact))
.build(); .build();
messageCallback.proofOfWorkStarted(response.getPayload()); messageCallback.proofOfWorkStarted(request.getPayload());
security.doProofOfWork(response, networkNonceTrialsPerByte, networkExtraBytes, proofOfWorkService.doProofOfWork(request);
new ProofOfWorkEngine.Callback() { }
@Override
public void onNonceCalculated(byte[] nonce) { private void tryToFindMatchingPubkey(BitmessageAddress address) {
response.setNonce(nonce); BitmessageAddress stored = addressRepository.getAddress(address.getAddress());
messageCallback.proofOfWorkCompleted(response.getPayload()); if (stored != null) {
inventory.storeObject(response); address.setAlias(stored.getAlias());
networkHandler.offer(response.getInventoryVector()); address.setSubscribed(stored.isSubscribed());
messageCallback.messageOffered(response.getPayload(), response.getInventoryVector()); }
for (ObjectMessage object : inventory.getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) {
try {
Pubkey pubkey = (Pubkey) object.getPayload();
if (address.getVersion() == 4) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
if (object.isSignatureValid(v4Pubkey)) {
address.setPubkey(v4Pubkey);
addressRepository.save(address);
break;
} else {
LOG.info("Found pubkey for " + address + " but signature is invalid");
}
}
} else {
if (Arrays.equals(pubkey.getRipe(), address.getRipe())) {
address.setPubkey(pubkey);
addressRepository.save(address);
break;
}
}
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
} }
});
} }
public long getClientNonce() { public long getClientNonce() {
@ -259,6 +300,10 @@ public class InternalContext {
return connectionLimit; return connectionLimit;
} }
public CustomCommandHandler getCustomCommandHandler() {
return customCommandHandler;
}
public interface ContextHolder { public interface ContextHolder {
void setContext(InternalContext context); void setContext(InternalContext context);
} }

View File

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

View File

@ -29,6 +29,8 @@ import java.util.List;
* The 'addr' command holds a list of known active Bitmessage nodes. * The 'addr' command holds a list of known active Bitmessage nodes.
*/ */
public class Addr implements MessagePayload { public class Addr implements MessagePayload {
private static final long serialVersionUID = -5117688017050138720L;
private final List<NetworkAddress> addresses; private final List<NetworkAddress> addresses;
private Addr(Builder builder) { private Addr(Builder builder) {
@ -53,10 +55,7 @@ public class Addr implements MessagePayload {
} }
public static final class Builder { public static final class Builder {
private List<NetworkAddress> addresses = new ArrayList<NetworkAddress>(); private List<NetworkAddress> addresses = new ArrayList<>();
public Builder() {
}
public Builder addresses(Collection<NetworkAddress> addresses){ public Builder addresses(Collection<NetworkAddress> addresses){
this.addresses.addAll(addresses); this.addresses.addAll(addresses);

View File

@ -20,6 +20,7 @@ import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; 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.utils.AccessCounter; import ch.dissem.bitmessage.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Base58; import ch.dissem.bitmessage.utils.Base58;
import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Bytes;
@ -29,7 +30,9 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import static ch.dissem.bitmessage.utils.Decode.bytes; import static ch.dissem.bitmessage.utils.Decode.bytes;
@ -41,6 +44,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* holding private keys. * holding private keys.
*/ */
public class BitmessageAddress implements Serializable { public class BitmessageAddress implements Serializable {
private static final long serialVersionUID = 2386328540805994064L;
private final long version; private final long version;
private final long stream; private final long stream;
private final byte[] ripe; private final byte[] ripe;
@ -57,6 +62,7 @@ public class BitmessageAddress implements Serializable {
private String alias; private String alias;
private boolean subscribed; private boolean subscribed;
private boolean chan;
BitmessageAddress(long version, long stream, byte[] ripe) { BitmessageAddress(long version, long stream, byte[] ripe) {
try { try {
@ -84,15 +90,47 @@ public class BitmessageAddress implements Serializable {
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) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
BitmessageAddress(Pubkey publicKey) { public BitmessageAddress(Pubkey publicKey) {
this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe()); this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe());
this.pubkey = publicKey; this.pubkey = publicKey;
} }
public BitmessageAddress(String address, String passphrase) {
this(address);
this.privateKey = new PrivateKey(this, passphrase);
this.pubkey = this.privateKey.getPubkey();
if (!Arrays.equals(ripe, privateKey.getPubkey().getRipe())) {
throw new IllegalArgumentException("Wrong address or passphrase");
}
}
public static BitmessageAddress chan(String address, String passphrase) {
BitmessageAddress result = new BitmessageAddress(address, passphrase);
result.chan = true;
return result;
}
public static BitmessageAddress chan(long stream, String passphrase) {
PrivateKey privateKey = new PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase);
BitmessageAddress result = new BitmessageAddress(privateKey);
result.chan = true;
return result;
}
public static List<BitmessageAddress> deterministic(String passphrase, int numberOfAddresses,
long version, long stream, boolean shorter) {
List<BitmessageAddress> result = new ArrayList<>(numberOfAddresses);
List<PrivateKey> privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter);
for (PrivateKey pk : privateKeys) {
result.add(new BitmessageAddress(pk));
}
return result;
}
public BitmessageAddress(PrivateKey privateKey) { public BitmessageAddress(PrivateKey privateKey) {
this(privateKey.getPubkey()); this(privateKey.getPubkey());
this.privateKey = privateKey; this.privateKey = privateKey;
@ -125,7 +163,7 @@ public class BitmessageAddress implements Serializable {
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -137,7 +175,7 @@ public class BitmessageAddress implements Serializable {
out.write(ripe); out.write(ripe);
return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64); return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -188,7 +226,7 @@ public class BitmessageAddress implements Serializable {
@Override @Override
public String toString() { public String toString() {
return alias != null ? alias : address; return alias == null ? address : alias;
} }
public byte[] getRipe() { public byte[] getRipe() {
@ -222,6 +260,14 @@ public class BitmessageAddress implements Serializable {
this.subscribed = subscribed; this.subscribed = subscribed;
} }
public boolean isChan() {
return chan;
}
public void setChan(boolean chan) {
this.chan = chan;
}
public boolean has(Feature feature) { public boolean has(Feature feature) {
if (pubkey == null || feature == null) { if (pubkey == null || feature == null) {
return false; return false;

View File

@ -0,0 +1,99 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Encode;
import java.io.*;
import static ch.dissem.bitmessage.utils.Decode.bytes;
import static ch.dissem.bitmessage.utils.Decode.varString;
/**
* @author Christian Basler
*/
public class CustomMessage implements MessagePayload {
private static final long serialVersionUID = -8932056829480326011L;
public static final String COMMAND_ERROR = "ERROR";
private final String command;
private final byte[] data;
public CustomMessage(String command) {
this.command = command;
this.data = null;
}
public CustomMessage(String command, byte[] data) {
this.command = command;
this.data = data;
}
public static CustomMessage read(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter();
return new CustomMessage(varString(in, counter), bytes(in, length - counter.length()));
}
@Override
public Command getCommand() {
return Command.CUSTOM;
}
public String getCustomCommand() {
return command;
}
public byte[] getData() {
if (data != null) {
return data;
} else {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
write(out);
return out.toByteArray();
} catch (IOException e) {
throw new ApplicationException(e);
}
}
}
@Override
public void write(OutputStream out) throws IOException {
if (data != null) {
Encode.varString(command, out);
out.write(data);
} else {
throw new ApplicationException("Tried to write custom message without data. " +
"Programmer: did you forget to override #write()?");
}
}
public boolean isError() {
return COMMAND_ERROR.equals(command);
}
public static CustomMessage error(String message) {
try {
return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
}
}

View File

@ -17,7 +17,6 @@
package ch.dissem.bitmessage.entity; package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.ports.Security;
import java.io.IOException; import java.io.IOException;

View File

@ -28,6 +28,10 @@ import java.util.List;
* The 'getdata' command is used to request objects from a node. * The 'getdata' command is used to request objects from a node.
*/ */
public class GetData implements MessagePayload { public class GetData implements MessagePayload {
private static final long serialVersionUID = 1433878785969631061L;
public static final int MAX_INVENTORY_SIZE = 50_000;
List<InventoryVector> inventory; List<InventoryVector> inventory;
private GetData(Builder builder) { private GetData(Builder builder) {
@ -44,19 +48,16 @@ public class GetData implements MessagePayload {
} }
@Override @Override
public void write(OutputStream stream) throws IOException { public void write(OutputStream out) throws IOException {
Encode.varInt(inventory.size(), stream); Encode.varInt(inventory.size(), out);
for (InventoryVector iv : inventory) { for (InventoryVector iv : inventory) {
iv.write(stream); iv.write(out);
} }
} }
public static final class Builder { public static final class Builder {
private List<InventoryVector> inventory = new LinkedList<>(); private List<InventoryVector> inventory = new LinkedList<>();
public Builder() {
}
public Builder addInventoryVector(InventoryVector inventoryVector) { public Builder addInventoryVector(InventoryVector inventoryVector) {
this.inventory.add(inventoryVector); this.inventory.add(inventoryVector);
return this; return this;

View File

@ -28,6 +28,8 @@ import java.util.List;
* The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items. * The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items.
*/ */
public class Inv implements MessagePayload { public class Inv implements MessagePayload {
private static final long serialVersionUID = 3662992522956947145L;
private List<InventoryVector> inventory; private List<InventoryVector> inventory;
private Inv(Builder builder) { private Inv(Builder builder) {
@ -44,19 +46,16 @@ public class Inv implements MessagePayload {
} }
@Override @Override
public void write(OutputStream stream) throws IOException { public void write(OutputStream out) throws IOException {
Encode.varInt(inventory.size(), stream); Encode.varInt(inventory.size(), out);
for (InventoryVector iv : inventory) { for (InventoryVector iv : inventory) {
iv.write(stream); iv.write(out);
} }
} }
public static final class Builder { public static final class Builder {
private List<InventoryVector> inventory = new LinkedList<>(); private List<InventoryVector> inventory = new LinkedList<>();
public Builder() {
}
public Builder addInventoryVector(InventoryVector inventoryVector) { public Builder addInventoryVector(InventoryVector inventoryVector) {
this.inventory.add(inventoryVector); this.inventory.add(inventoryVector);
return this; return this;

View File

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

View File

@ -16,6 +16,7 @@
package ch.dissem.bitmessage.entity; package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -32,6 +33,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* A network message is exchanged between two nodes. * A network message is exchanged between two nodes.
*/ */
public class NetworkMessage implements Streamable { public class NetworkMessage implements Streamable {
private static final long serialVersionUID = 702708857104464809L;
/** /**
* Magic value indicating message origin network, and used to seek to next message when stream state is unknown * Magic value indicating message origin network, and used to seek to next message when stream state is unknown
*/ */
@ -84,7 +87,7 @@ public class NetworkMessage implements Streamable {
try { try {
out.write(getChecksum(payloadBytes)); out.write(getChecksum(payloadBytes));
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
// message payload // message payload

View File

@ -21,8 +21,8 @@ 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.entity.valueobject.InventoryVector;
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.ports.Security;
import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
@ -36,6 +36,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* The 'object' command sends an object that is shared throughout the network. * The 'object' command sends an object that is shared throughout the network.
*/ */
public class ObjectMessage implements MessagePayload { public class ObjectMessage implements MessagePayload {
private static final long serialVersionUID = 2495752480120659139L;
private byte[] nonce; private byte[] nonce;
private long expiresTime; private long expiresTime;
private long objectType; private long objectType;
@ -111,7 +113,7 @@ public class ObjectMessage implements MessagePayload {
payload.writeBytesToSign(out); payload.writeBytesToSign(out);
return out.toByteArray(); return out.toByteArray();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -145,7 +147,7 @@ public class ObjectMessage implements MessagePayload {
((Encrypted) payload).encrypt(publicKey.getEncryptionKey()); ((Encrypted) payload).encrypt(publicKey.getEncryptionKey());
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -156,7 +158,11 @@ public class ObjectMessage implements MessagePayload {
@Override @Override
public void write(OutputStream out) throws IOException { public void write(OutputStream out) throws IOException {
if (nonce == null) {
out.write(new byte[8]);
} else {
out.write(nonce); out.write(nonce);
}
out.write(getPayloadBytesWithoutNonce()); out.write(getPayloadBytesWithoutNonce());
} }
@ -177,7 +183,7 @@ public class ObjectMessage implements MessagePayload {
} }
return payloadBytes; return payloadBytes;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -188,9 +194,6 @@ public class ObjectMessage implements MessagePayload {
private long streamNumber; private long streamNumber;
private ObjectPayload payload; private ObjectPayload payload;
public Builder() {
}
public Builder nonce(byte[] nonce) { public Builder nonce(byte[] nonce) {
this.nonce = nonce; this.nonce = nonce;
return this; return this;

View File

@ -19,6 +19,7 @@ package ch.dissem.bitmessage.entity;
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.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.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
@ -33,6 +34,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* The unencrypted message to be sent by 'msg' or 'broadcast'. * The unencrypted message to be sent by 'msg' or 'broadcast'.
*/ */
public class Plaintext implements Streamable { public class Plaintext implements Streamable {
private static final long serialVersionUID = -5325729856394951079L;
private final Type type; private final Type type;
private final BitmessageAddress from; private final BitmessageAddress from;
private final long encoding; private final long encoding;
@ -48,6 +51,7 @@ public class Plaintext implements Streamable {
private Long received; private Long received;
private Set<Label> labels; private Set<Label> labels;
private byte[] initialHash;
private Plaintext(Builder builder) { private Plaintext(Builder builder) {
id = builder.id; id = builder.id;
@ -120,9 +124,9 @@ public class Plaintext implements Streamable {
public void setTo(BitmessageAddress to) { public void setTo(BitmessageAddress to) {
if (this.to.getVersion() != 0) if (this.to.getVersion() != 0)
throw new RuntimeException("Correct address already set"); throw new IllegalStateException("Correct address already set");
if (!Arrays.equals(this.to.getRipe(), to.getRipe())) { if (!Arrays.equals(this.to.getRipe(), to.getRipe())) {
throw new RuntimeException("RIPEs don't match"); throw new IllegalArgumentException("RIPEs don't match");
} }
this.to = to; this.to = to;
} }
@ -239,7 +243,7 @@ public class Plaintext implements Streamable {
} }
return text; return text;
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -302,6 +306,14 @@ public class Plaintext implements Streamable {
return ackMessage; return ackMessage;
} }
public void setInitialHash(byte[] initialHash) {
this.initialHash = initialHash;
}
public byte[] getInitialHash() {
return initialHash;
}
public enum Encoding { public enum Encoding {
IGNORE(0), TRIVIAL(1), SIMPLE(2); IGNORE(0), TRIVIAL(1), SIMPLE(2);
@ -436,7 +448,7 @@ public class Plaintext implements Streamable {
this.encoding = Encoding.SIMPLE.getCode(); this.encoding = Encoding.SIMPLE.getCode();
this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8"); this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
return this; return this;
} }
@ -495,7 +507,7 @@ public class Plaintext implements Streamable {
behaviorBitfield behaviorBitfield
)); ));
} }
if (to == null && type != Type.BROADCAST) { 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) { if (type == Type.MSG && ackMessage == null && ackData == null) {

View File

@ -23,6 +23,8 @@ import java.io.OutputStream;
* The 'verack' command answers a 'version' command, accepting the other node's version. * The 'verack' command answers a 'version' command, accepting the other node's version.
*/ */
public class VerAck implements MessagePayload { public class VerAck implements MessagePayload {
private static final long serialVersionUID = -4302074845199181687L;
@Override @Override
public Command getCommand() { public Command getCommand() {
return Command.VERACK; return Command.VERACK;

View File

@ -29,6 +29,8 @@ import java.util.Random;
* The 'version' command advertises this node's latest supported protocol version upon initiation. * The 'version' command advertises this node's latest supported protocol version upon initiation.
*/ */
public class Version implements MessagePayload { public class Version implements MessagePayload {
private static final long serialVersionUID = 7219240857343176567L;
/** /**
* Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's
* version is lower but continue with the connection if it is higher. * version is lower but continue with the connection if it is higher.
@ -143,9 +145,6 @@ public class Version implements MessagePayload {
private String userAgent; private String userAgent;
private long[] streamNumbers; private long[] streamNumbers;
public Builder() {
}
public Builder defaults() { public Builder defaults() {
version = BitmessageContext.CURRENT_VERSION; version = BitmessageContext.CURRENT_VERSION;
services = 1; services = 1;

View File

@ -21,7 +21,6 @@ import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder; import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.ports.Security;
import java.io.IOException; import java.io.IOException;
@ -33,6 +32,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* Broadcasts are version 4 or 5. * Broadcasts are version 4 or 5.
*/ */
public abstract class Broadcast extends ObjectPayload implements Encrypted, PlaintextHolder { public abstract class Broadcast extends ObjectPayload implements Encrypted, PlaintextHolder {
private static final long serialVersionUID = 4064521827582239069L;
protected final long stream; protected final long stream;
protected CryptoBox encrypted; protected CryptoBox encrypted;
protected Plaintext plaintext; protected Plaintext plaintext;

View File

@ -17,6 +17,7 @@
package ch.dissem.bitmessage.entity.payload; package ch.dissem.bitmessage.entity.payload;
import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.*; import ch.dissem.bitmessage.utils.*;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -30,6 +31,7 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
public class CryptoBox implements Streamable { public class CryptoBox implements Streamable {
private static final long serialVersionUID = 7217659539975573852L;
private static final Logger LOG = LoggerFactory.getLogger(CryptoBox.class); private static final Logger LOG = LoggerFactory.getLogger(CryptoBox.class);
private final byte[] initializationVector; private final byte[] initializationVector;
@ -38,7 +40,12 @@ public class CryptoBox implements Streamable {
private final byte[] mac; private final byte[] mac;
private byte[] encrypted; private byte[] encrypted;
public CryptoBox(Streamable data, byte[] K) throws IOException { public CryptoBox(Streamable data, byte[] K) throws IOException {
this(Encode.bytes(data), K);
}
public CryptoBox(byte[] data, byte[] K) throws IOException {
curveType = 0x02CA; curveType = 0x02CA;
// 1. The destination public key is called K. // 1. The destination public key is called K.
@ -58,7 +65,7 @@ public class CryptoBox implements Streamable {
byte[] key_m = Arrays.copyOfRange(H, 32, 64); 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, Encode.bytes(data), key_e, initializationVector); encrypted = security().crypt(true, data, key_e, initializationVector);
// 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. // 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);
@ -118,7 +125,7 @@ public class CryptoBox implements Streamable {
writeWithoutMAC(macData); writeWithoutMAC(macData);
return security().mac(key_m, macData.toByteArray()); return security().mac(key_m, macData.toByteArray());
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }

View File

@ -28,6 +28,8 @@ import java.util.Arrays;
* have to know what it is. * have to know what it is.
*/ */
public class GenericPayload extends ObjectPayload { public class GenericPayload extends ObjectPayload {
private static final long serialVersionUID = -912314085064185940L;
private long stream; private long stream;
private byte[] data; private byte[] data;

View File

@ -27,6 +27,8 @@ import java.io.OutputStream;
* Request for a public key. * Request for a public key.
*/ */
public class GetPubkey extends ObjectPayload { public class GetPubkey extends ObjectPayload {
private static final long serialVersionUID = -3634516646972610180L;
private long stream; private long stream;
private byte[] ripeTag; private byte[] ripeTag;

View File

@ -31,6 +31,8 @@ import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
* Used for person-to-person messages. * Used for person-to-person messages.
*/ */
public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder { public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder {
private static final long serialVersionUID = 4327495048296365733L;
private long stream; private long stream;
private CryptoBox encrypted; private CryptoBox encrypted;
private Plaintext plaintext; private Plaintext plaintext;

View File

@ -21,12 +21,13 @@ import ch.dissem.bitmessage.entity.Streamable;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Serializable;
/** /**
* The payload of an 'object' command. This is shared by the network. * The payload of an 'object' command. This is shared by the network.
*/ */
public abstract class ObjectPayload implements Streamable { public abstract class ObjectPayload implements Streamable {
private static final long serialVersionUID = -5034977402902364482L;
private final long version; private final long version;
protected ObjectPayload(long version) { protected ObjectPayload(long version) {

View File

@ -26,6 +26,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* Public keys for signing and encryption, the answer to a 'getpubkey' request. * Public keys for signing and encryption, the answer to a 'getpubkey' request.
*/ */
public abstract class Pubkey extends ObjectPayload { public abstract class Pubkey extends ObjectPayload {
private static final long serialVersionUID = -6634533361454999619L;
public final static long LATEST_VERSION = 4; public final static long LATEST_VERSION = 4;
protected Pubkey(long version) { protected Pubkey(long version) {

View File

@ -27,6 +27,8 @@ import java.io.OutputStream;
* A version 2 public key. * A version 2 public key.
*/ */
public class V2Pubkey extends Pubkey { public class V2Pubkey extends Pubkey {
private static final long serialVersionUID = -257598690676510460L;
protected long stream; protected long stream;
protected int behaviorBitfield; protected int behaviorBitfield;
protected byte[] publicSigningKey; // 64 Bytes protected byte[] publicSigningKey; // 64 Bytes
@ -96,9 +98,6 @@ public class V2Pubkey extends Pubkey {
private byte[] publicSigningKey; private byte[] publicSigningKey;
private byte[] publicEncryptionKey; private byte[] publicEncryptionKey;
public Builder() {
}
public Builder stream(long streamNumber) { public Builder stream(long streamNumber) {
this.streamNumber = streamNumber; this.streamNumber = streamNumber;
return this; return this;

View File

@ -29,6 +29,8 @@ import java.util.Objects;
* A version 3 public key. * A version 3 public key.
*/ */
public class V3Pubkey extends V2Pubkey { public class V3Pubkey extends V2Pubkey {
private static final long serialVersionUID = 6958853116648528319L;
long nonceTrialsPerByte; long nonceTrialsPerByte;
long extraBytes; long extraBytes;
byte[] signature; byte[] signature;
@ -123,9 +125,6 @@ public class V3Pubkey extends V2Pubkey {
private long extraBytes; private long extraBytes;
private byte[] signature = new byte[0]; private byte[] signature = new byte[0];
public Builder() {
}
public Builder stream(long streamNumber) { public Builder stream(long streamNumber) {
this.streamNumber = streamNumber; this.streamNumber = streamNumber;
return this; return this;

View File

@ -18,7 +18,6 @@ package ch.dissem.bitmessage.entity.payload;
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.ports.Security;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -29,6 +28,8 @@ import java.io.OutputStream;
* Broadcasts are version 4 or 5. * Broadcasts are version 4 or 5.
*/ */
public class V4Broadcast extends Broadcast { public class V4Broadcast extends Broadcast {
private static final long serialVersionUID = 195663108282762711L;
protected V4Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) { protected V4Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) {
super(version, stream, encrypted, plaintext); super(version, stream, encrypted, plaintext);
} }

View File

@ -33,6 +33,8 @@ import java.util.Arrays;
* to create messages to be used in spam or in flooding attacks. * to create messages to be used in spam or in flooding attacks.
*/ */
public class V4Pubkey extends Pubkey implements Encrypted { public class V4Pubkey extends Pubkey implements Encrypted {
private static final long serialVersionUID = 1556710353694033093L;
private long stream; private long stream;
private byte[] tag; private byte[] tag;
private CryptoBox encrypted; private CryptoBox encrypted;

View File

@ -28,6 +28,8 @@ import java.io.OutputStream;
* 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.
*/ */
public class V5Broadcast extends V4Broadcast { public class V5Broadcast extends V4Broadcast {
private static final long serialVersionUID = 920649721626968644L;
private byte[] tag; private byte[] tag;
private V5Broadcast(long stream, byte[] tag, CryptoBox encrypted) { private V5Broadcast(long stream, byte[] tag, CryptoBox encrypted) {

View File

@ -25,6 +25,8 @@ import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
public class InventoryVector implements Streamable, Serializable { public class InventoryVector implements Streamable, Serializable {
private static final long serialVersionUID = -7349009673063348719L;
/** /**
* Hash of the object * Hash of the object
*/ */
@ -38,12 +40,11 @@ public class InventoryVector implements Streamable, Serializable {
InventoryVector that = (InventoryVector) o; InventoryVector that = (InventoryVector) o;
return Arrays.equals(hash, that.hash); return Arrays.equals(hash, that.hash);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return hash != null ? Arrays.hashCode(hash) : 0; return hash == null ? 0 : Arrays.hashCode(hash);
} }
public byte[] getHash() { public byte[] getHash() {

View File

@ -20,6 +20,8 @@ import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
public class Label implements Serializable { public class Label implements Serializable {
private static final long serialVersionUID = 831782893630994914L;
private Object id; private Object id;
private String label; private String label;
private Type type; private Type type;

View File

@ -17,6 +17,7 @@
package ch.dissem.bitmessage.entity.valueobject; package ch.dissem.bitmessage.entity.valueobject;
import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.UnixTime; import ch.dissem.bitmessage.utils.UnixTime;
@ -30,6 +31,8 @@ import java.util.Arrays;
* A node's address. It's written in IPv6 format. * A node's address. It's written in IPv6 format.
*/ */
public class NetworkAddress implements Streamable { public class NetworkAddress implements Streamable {
private static final long serialVersionUID = 2500120578167100300L;
private long time; private long time;
/** /**
@ -85,7 +88,7 @@ public class NetworkAddress implements Streamable {
try { try {
return InetAddress.getByAddress(ipv6); return InetAddress.getByAddress(ipv6);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -133,9 +136,6 @@ public class NetworkAddress implements Streamable {
private byte[] ipv6; private byte[] ipv6;
private int port; private int port;
public Builder() {
}
public Builder time(final long time) { public Builder time(final long time) {
this.time = time; this.time = time;
return this; return this;

View File

@ -16,14 +16,19 @@
package ch.dissem.bitmessage.entity.valueobject; package ch.dissem.bitmessage.entity.valueobject;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
import java.io.*; import java.io.*;
import java.util.ArrayList;
import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.Singleton.security;
@ -32,7 +37,10 @@ import static ch.dissem.bitmessage.utils.Singleton.security;
* {@link Pubkey} object. * {@link Pubkey} object.
*/ */
public class PrivateKey implements Streamable { public class PrivateKey implements Streamable {
private static final long serialVersionUID = 8562555470709110558L;
public static final int PRIVATE_KEY_SIZE = 32; public static final int PRIVATE_KEY_SIZE = 32;
private final byte[] privateSigningKey; private final byte[] privateSigningKey;
private final byte[] privateEncryptionKey; private final byte[] privateEncryptionKey;
@ -63,16 +71,78 @@ public class PrivateKey implements Streamable {
this.pubkey = pubkey; this.pubkey = pubkey;
} }
public PrivateKey(long version, long stream, String passphrase, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { public PrivateKey(BitmessageAddress address, String passphrase) {
try { this(address.getVersion(), address.getStream(), passphrase);
// FIXME: this is most definitely wrong
this.privateSigningKey = Bytes.truncate(security().sha512(passphrase.getBytes("UTF-8"), new byte[]{0}), 32);
this.privateEncryptionKey = Bytes.truncate(security().sha512(passphrase.getBytes("UTF-8"), new byte[]{1}), 32);
this.pubkey = security().createPubkey(version, stream, privateSigningKey, privateEncryptionKey,
nonceTrialsPerByte, extraBytes, features);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} }
public PrivateKey(long version, long stream, String passphrase) {
this(new Builder(version, stream, false).seed(passphrase).generate());
}
private PrivateKey(Builder builder) {
this.privateSigningKey = builder.privSK;
this.privateEncryptionKey = builder.privEK;
this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK, builder.pubEK,
InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES);
}
private static class Builder {
final long version;
final long stream;
final boolean shorter;
byte[] seed;
long nextNonce;
byte[] privSK, privEK;
byte[] pubSK, pubEK;
private Builder(long version, long stream, boolean shorter) {
this.version = version;
this.stream = stream;
this.shorter = shorter;
}
Builder seed(String passphrase) {
try {
seed = passphrase.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
return this;
}
Builder generate() {
try {
long signingKeyNonce = nextNonce;
long encryptionKeyNonce = nextNonce + 1;
byte[] ripe;
do {
privEK = Bytes.truncate(security().sha512(seed, Encode.varInt(encryptionKeyNonce)), 32);
privSK = Bytes.truncate(security().sha512(seed, Encode.varInt(signingKeyNonce)), 32);
pubSK = security().createPublicKey(privSK);
pubEK = security().createPublicKey(privEK);
ripe = security().ripemd160(security().sha512(pubSK, pubEK));
signingKeyNonce += 2;
encryptionKeyNonce += 2;
} while (ripe[0] != 0 || (shorter && ripe[1] != 0));
nextNonce = signingKeyNonce;
} catch (IOException e) {
throw new ApplicationException(e);
}
return this;
}
}
public static List<PrivateKey> deterministic(String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) {
List<PrivateKey> result = new ArrayList<>(numberOfAddresses);
Builder builder = new Builder(version, stream, shorter).seed(passphrase);
for (int i = 0; i < numberOfAddresses; i++) {
builder.generate();
result.add(new PrivateKey(builder));
}
return result;
} }
public static PrivateKey read(InputStream is) throws IOException { public static PrivateKey read(InputStream is) throws IOException {

View File

@ -20,6 +20,8 @@ package ch.dissem.bitmessage.exception;
* Indicates an illegal Bitmessage address * Indicates an illegal Bitmessage address
*/ */
public class AddressFormatException extends RuntimeException { public class AddressFormatException extends RuntimeException {
private static final long serialVersionUID = 6943764578672021573L;
public AddressFormatException(String message) { public AddressFormatException(String message) {
super(message); super(message);
} }

View File

@ -0,0 +1,32 @@
/*
* 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.exception;
/**
* @author Christian Basler
*/
public class ApplicationException extends RuntimeException {
private static final long serialVersionUID = 1796776684126759324L;
public ApplicationException(Throwable cause) {
super(cause);
}
public ApplicationException(String message) {
super(message);
}
}

View File

@ -17,4 +17,5 @@
package ch.dissem.bitmessage.exception; package ch.dissem.bitmessage.exception;
public class DecryptionFailedException extends Exception { public class DecryptionFailedException extends Exception {
private static final long serialVersionUID = 3241116253113872731L;
} }

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
public class InsufficientProofOfWorkException extends IOException { public class InsufficientProofOfWorkException extends IOException {
private static final long serialVersionUID = 9105580366564571318L;
public InsufficientProofOfWorkException(byte[] target, byte[] hash) { public InsufficientProofOfWorkException(byte[] target, byte[] hash) {
super("Insufficient proof of work: " + Strings.hex(target) + " required, " + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved."); super("Insufficient proof of work: " + Strings.hex(target) + " required, " + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved.");
} }

View File

@ -22,6 +22,8 @@ package ch.dissem.bitmessage.exception;
* @author Ch. Basler * @author Ch. Basler
*/ */
public class NodeException extends RuntimeException { public class NodeException extends RuntimeException {
private static final long serialVersionUID = 2965325796118227802L;
public NodeException(String message) { public NodeException(String message) {
super(message); super(message);
} }

View File

@ -196,7 +196,8 @@ public class Factory {
} }
} }
public static ObjectPayload getBroadcast(BitmessageAddress sendingAddress, Plaintext plaintext) { public static Broadcast getBroadcast(Plaintext plaintext) {
BitmessageAddress sendingAddress = plaintext.getFrom();
if (sendingAddress.getVersion() < 4) { if (sendingAddress.getVersion() < 4) {
return new V4Broadcast(sendingAddress, plaintext); return new V4Broadcast(sendingAddress, plaintext);
} else { } else {

View File

@ -44,6 +44,9 @@ class V3MessageFactory {
findMagic(in); findMagic(in);
String command = getCommand(in); String command = getCommand(in);
int length = (int) Decode.uint32(in); int length = (int) Decode.uint32(in);
if (length > 1600003) {
throw new NodeException("Payload of " + length + " bytes received, no more than 1600003 was expected.");
}
byte[] checksum = Decode.bytes(in, 4); byte[] checksum = Decode.bytes(in, 4);
byte[] payloadBytes = Decode.bytes(in, length); byte[] payloadBytes = Decode.bytes(in, length);
@ -73,12 +76,18 @@ class V3MessageFactory {
return parseGetData(stream); return parseGetData(stream);
case "object": case "object":
return readObject(stream, length); return readObject(stream, length);
case "custom":
return readCustom(stream, length);
default: default:
LOG.debug("Unknown command: " + command); LOG.debug("Unknown command: " + command);
return null; return null;
} }
} }
private static MessagePayload readCustom(InputStream in, int length) throws IOException {
return CustomMessage.read(in, length);
}
public static ObjectMessage readObject(InputStream in, int length) throws IOException { public static ObjectMessage readObject(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter(); AccessCounter counter = new AccessCounter();
byte nonce[] = Decode.bytes(in, 8, counter); byte nonce[] = Decode.bytes(in, 8, counter);
@ -185,10 +194,10 @@ class V3MessageFactory {
private static String getCommand(InputStream stream) throws IOException { private static String getCommand(InputStream stream) throws IOException {
byte[] bytes = new byte[12]; byte[] bytes = new byte[12];
int end = -1; int end = bytes.length;
for (int i = 0; i < bytes.length; i++) { for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) stream.read(); bytes[i] = (byte) stream.read();
if (end == -1) { if (end == bytes.length) {
if (bytes[i] == 0) end = i; if (bytes[i] == 0) end = i;
} else { } else {
if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command"); if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command");

View File

@ -19,6 +19,7 @@ package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Bytes;
@ -34,18 +35,24 @@ import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.SecureRandom; import java.security.SecureRandom;
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.utils.Numbers.max;
/** /**
* Implements everything that isn't directly dependent on either Spongy- or Bouncycastle. * Implements everything that isn't directly dependent on either Spongy- or Bouncycastle.
*/ */
public abstract class AbstractSecurity implements Security, InternalContext.ContextHolder { public abstract class AbstractCryptography implements Cryptography, InternalContext.ContextHolder {
public static final Logger LOG = LoggerFactory.getLogger(Security.class); public static final Logger LOG = LoggerFactory.getLogger(Cryptography.class);
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
private static final BigInteger TWO = BigInteger.valueOf(2); private static final BigInteger TWO = BigInteger.valueOf(2);
private static final BigInteger TWO_POW_64 = TWO.pow(64);
private static final BigInteger TWO_POW_16 = TWO.pow(16);
private final String provider; private final String provider;
private InternalContext context; private InternalContext context;
protected AbstractSecurity(String provider) { protected AbstractCryptography(String provider) {
this.provider = provider; this.provider = provider;
} }
@ -94,18 +101,14 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte,
long extraBytes, ProofOfWorkEngine.Callback callback) { long extraBytes, ProofOfWorkEngine.Callback callback) {
try { nonceTrialsPerByte = max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE);
if (nonceTrialsPerByte < 1000) nonceTrialsPerByte = 1000; extraBytes = max(extraBytes, NETWORK_EXTRA_BYTES);
if (extraBytes < 1000) extraBytes = 1000;
byte[] initialHash = getInitialHash(object); byte[] initialHash = getInitialHash(object);
byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes);
context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback); context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback);
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
@ -117,15 +120,25 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
} }
} }
private byte[] getInitialHash(ObjectMessage object) throws IOException { @Override
public byte[] getInitialHash(ObjectMessage object) {
return sha512(object.getPayloadBytesWithoutNonce()); return sha512(object.getPayloadBytesWithoutNonce());
} }
private byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException { @Override
public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
if (nonceTrialsPerByte == 0) nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE;
if (extraBytes == 0) extraBytes = NETWORK_EXTRA_BYTES;
BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now()); BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now());
BigInteger numerator = TWO.pow(64); BigInteger numerator = TWO_POW_64;
BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes); BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes);
BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte).multiply(powLength.add(powLength.multiply(TTL).divide(BigInteger.valueOf(2).pow(16)))); BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte)
.multiply(
powLength.add(
powLength.multiply(TTL).divide(TWO_POW_16)
)
);
return Bytes.expand(numerator.divide(denominator).toByteArray(), 8); return Bytes.expand(numerator.divide(denominator).toByteArray(), 8);
} }
@ -141,7 +154,7 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
try { try {
return MessageDigest.getInstance(algorithm, provider); return MessageDigest.getInstance(algorithm, provider);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -151,7 +164,7 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
mac.init(new SecretKeySpec(key_m, "HmacSHA256")); mac.init(new SecretKeySpec(key_m, "HmacSHA256"));
return mac.doFinal(data); return mac.doFinal(data);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }

View File

@ -30,12 +30,17 @@ public interface AddressRepository {
*/ */
List<BitmessageAddress> getIdentities(); List<BitmessageAddress> getIdentities();
/**
* @return all subscribed chans.
*/
List<BitmessageAddress> getChans();
List<BitmessageAddress> getSubscriptions(); List<BitmessageAddress> getSubscriptions();
List<BitmessageAddress> getSubscriptions(long broadcastVersion); List<BitmessageAddress> getSubscriptions(long broadcastVersion);
/** /**
* @return all Bitmessage addresses that have no private key. * @return all Bitmessage addresses that have no private key or are chans.
*/ */
List<BitmessageAddress> getContacts(); List<BitmessageAddress> getContacts();

View File

@ -29,7 +29,7 @@ import java.security.SecureRandom;
* Provides some methods to help with hashing and encryption. All randoms are created using {@link SecureRandom}, * Provides some methods to help with hashing and encryption. All randoms are created using {@link SecureRandom},
* which should be secure enough. * which should be secure enough.
*/ */
public interface Security { public interface Cryptography {
/** /**
* A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at
* each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in
@ -134,6 +134,10 @@ public interface Security {
void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
throws IOException; throws IOException;
byte[] getInitialHash(ObjectMessage object);
byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
/** /**
* Calculates the MAC for a message (data) * Calculates the MAC for a message (data)
* *

View File

@ -0,0 +1,27 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.MessagePayload;
/**
* @author Christian Basler
*/
public interface CustomCommandHandler {
MessagePayload handle(CustomMessage request);
}

View File

@ -0,0 +1,68 @@
/*
* 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.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label;
import java.util.Iterator;
public class DefaultLabeler implements Labeler, InternalContext.ContextHolder {
private InternalContext ctx;
@Override
public void setLabels(Plaintext msg) {
if (msg.getType() == Plaintext.Type.BROADCAST) {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
} else {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD));
}
}
@Override
public void markAsRead(Plaintext msg) {
Iterator<Label> iterator = msg.getLabels().iterator();
while (iterator.hasNext()) {
Label label = iterator.next();
if (label.getType() == Label.Type.UNREAD) {
iterator.remove();
}
}
}
@Override
public void markAsUnread(Plaintext msg) {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.UNREAD));
}
@Override
public void delete(Plaintext msg) {
msg.getLabels().clear();
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.TRASH));
}
@Override
public void archive(Plaintext msg) {
msg.getLabels().clear();
}
@Override
public void setContext(InternalContext ctx) {
this.ctx = ctx;
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.Plaintext;
/**
* Defines and sets labels
*/
public interface Labeler {
/**
* Sets the labels of a newly received message.
*
* @param msg an unlabeled message or broadcast
*/
void setLabels(Plaintext msg);
void markAsRead(Plaintext msg);
void markAsUnread(Plaintext msg);
void delete(Plaintext msg);
void archive(Plaintext msg);
}

View File

@ -17,6 +17,7 @@
package ch.dissem.bitmessage.ports; package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.UnixTime; import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -71,7 +72,7 @@ public class MemoryNodeRegistry implements NodeRegistry {
} }
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }

View File

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

View File

@ -16,6 +16,7 @@
package ch.dissem.bitmessage.ports; package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Bytes;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -49,7 +50,7 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
try { try {
semaphore.acquire(); semaphore.acquire();
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new ApplicationException(e);
} }
callback = new CallbackWrapper(callback); callback = new CallbackWrapper(callback);
int cores = Runtime.getRuntime().availableProcessors(); int cores = Runtime.getRuntime().availableProcessors();
@ -88,7 +89,7 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
mda = MessageDigest.getInstance("SHA-512"); mda = MessageDigest.getInstance("SHA-512");
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
LOG.error(e.getMessage(), e); LOG.error(e.getMessage(), e);
throw new RuntimeException(e); throw new ApplicationException(e);
} }
} }
@ -101,14 +102,12 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) { if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) {
synchronized (callback) { synchronized (callback) {
if (!Thread.interrupted()) { if (!Thread.interrupted()) {
try {
callback.onNonceCalculated(nonce);
} finally {
semaphore.release();
for (Worker w : workers) { for (Worker w : workers) {
w.interrupt(); w.interrupt();
} }
} // Clear interrupted flag for callback
Thread.interrupted();
callback.onNonceCalculated(initialHash, nonce);
} }
} }
return; return;
@ -128,12 +127,14 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
} }
@Override @Override
public void onNonceCalculated(byte[] nonce) { public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
// Prevents the callback from being called twice if two nonces are found simultaneously
synchronized (this) { synchronized (this) {
if (waiting) { if (waiting) {
semaphore.release();
LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds"); LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds");
waiting = false; waiting = false;
callback.onNonceCalculated(nonce); callback.onNonceCalculated(initialHash, nonce);
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -16,30 +16,35 @@
package ch.dissem.bitmessage.ports; package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Bytes;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static ch.dissem.bitmessage.utils.Bytes.inc; import static ch.dissem.bitmessage.utils.Bytes.inc;
/** /**
* You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one. * You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one.
* <p>
* <strong>Warning:</strong> implementations probably depend on POW being asynchronous, that's
* another reason not to use this one.
* </p>
*/ */
public class SimplePOWEngine implements ProofOfWorkEngine { public class SimplePOWEngine implements ProofOfWorkEngine {
@Override @Override
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
byte[] nonce = new byte[8];
MessageDigest mda;
try { try {
mda = MessageDigest.getInstance("SHA-512"); MessageDigest mda = MessageDigest.getInstance("SHA-512");
} catch (Exception e) { byte[] nonce = new byte[8];
throw new RuntimeException(e);
}
do { do {
inc(nonce); inc(nonce);
mda.update(nonce); mda.update(nonce);
mda.update(initialHash); mda.update(initialHash);
} while (Bytes.lt(target, mda.digest(mda.digest()), 8)); } while (Bytes.lt(target, mda.digest(mda.digest()), 8));
callback.onNonceCalculated(nonce); callback.onNonceCalculated(initialHash, nonce);
} catch (NoSuchAlgorithmException e) {
throw new ApplicationException(e);
}
} }
} }

View File

@ -18,6 +18,7 @@
package ch.dissem.bitmessage.utils; package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.exception.AddressFormatException; import ch.dissem.bitmessage.exception.AddressFormatException;
import ch.dissem.bitmessage.exception.ApplicationException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -30,7 +31,7 @@ import static java.util.Arrays.copyOfRange;
*/ */
public class Base58 { public class Base58 {
private static final int[] INDEXES = new int[128]; private static final int[] INDEXES = new int[128];
private static char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
static { static {
for (int i = 0; i < INDEXES.length; i++) { for (int i = 0; i < INDEXES.length; i++) {
@ -44,27 +45,27 @@ public class Base58 {
/** /**
* Encodes the given bytes in base58. No checksum is appended. * Encodes the given bytes in base58. No checksum is appended.
* *
* @param input to encode * @param data to encode
* @return base58 encoded input * @return base58 encoded input
*/ */
public static String encode(byte[] input) { public static String encode(byte[] data) {
if (input.length == 0) { if (data.length == 0) {
return ""; return "";
} }
input = copyOfRange(input, 0, input.length); final byte[] bytes = copyOfRange(data, 0, data.length);
// Count leading zeroes. // Count leading zeroes.
int zeroCount = 0; int zeroCount = 0;
while (zeroCount < input.length && input[zeroCount] == 0) { while (zeroCount < bytes.length && bytes[zeroCount] == 0) {
++zeroCount; ++zeroCount;
} }
// The actual encoding. // The actual encoding.
byte[] temp = new byte[input.length * 2]; byte[] temp = new byte[bytes.length * 2];
int j = temp.length; int j = temp.length;
int startAt = zeroCount; int startAt = zeroCount;
while (startAt < input.length) { while (startAt < bytes.length) {
byte mod = divmod58(input, startAt); byte mod = divmod58(bytes, startAt);
if (input[startAt] == 0) { if (bytes[startAt] == 0) {
++startAt; ++startAt;
} }
temp[--j] = (byte) ALPHABET[mod]; temp[--j] = (byte) ALPHABET[mod];
@ -83,7 +84,7 @@ public class Base58 {
try { try {
return new String(output, "US-ASCII"); return new String(output, "US-ASCII");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Cannot happen. throw new ApplicationException(e); // Cannot happen.
} }
} }
@ -97,7 +98,7 @@ public class Base58 {
char c = input.charAt(i); char c = input.charAt(i);
int digit58 = -1; int digit58 = -1;
if (c >= 0 && c < 128) { if (c < 128) {
digit58 = INDEXES[c]; digit58 = INDEXES[c];
} }
if (digit58 < 0) { if (digit58 < 0) {

View File

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

View File

@ -41,6 +41,34 @@ public class Encode {
varInt(value, stream, null); varInt(value, stream, null);
} }
public static byte[] varInt(long value) throws IOException {
final byte[] result;
if (value < 0) {
// This is due to the fact that Java doesn't really support unsigned values.
// Please be aware that this might be an error due to a smaller negative value being cast to long.
// Normally, negative values shouldn't occur within the protocol, and I large enough longs
// to being recognized as negatives aren't realistic.
ByteBuffer buffer = ByteBuffer.allocate(9);
buffer.put((byte) 0xff);
result = buffer.putLong(value).array();
} else if (value < 0xfd) {
result = new byte[]{(byte) value};
} else if (value <= 0xffffL) {
ByteBuffer buffer = ByteBuffer.allocate(3);
buffer.put((byte) 0xfd);
result = buffer.putShort((short) value).array();
} else if (value <= 0xffffffffL) {
ByteBuffer buffer = ByteBuffer.allocate(5);
buffer.put((byte) 0xfe);
result = buffer.putInt((int) value).array();
} else {
ByteBuffer buffer = ByteBuffer.allocate(9);
buffer.put((byte) 0xff);
result = buffer.putLong(value).array();
}
return result;
}
public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException { public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException {
if (value < 0) { if (value < 0) {
// This is due to the fact that Java doesn't really support unsigned values. // This is due to the fact that Java doesn't really support unsigned values.
@ -81,7 +109,7 @@ public class Encode {
} }
public static void int16(long value, OutputStream stream, AccessCounter counter) throws IOException { public static void int16(long value, OutputStream stream, AccessCounter counter) throws IOException {
stream.write(ByteBuffer.allocate(4).putInt((int) value).array(), 2, 2); stream.write(ByteBuffer.allocate(2).putShort((short) value).array());
inc(counter, 2); inc(counter, 2);
} }
@ -103,15 +131,23 @@ public class Encode {
inc(counter, 8); inc(counter, 8);
} }
public static void varString(String value, OutputStream stream) throws IOException { public static void varString(String value, OutputStream out) throws IOException {
byte[] bytes = value.getBytes("utf-8"); byte[] bytes = value.getBytes("utf-8");
// FIXME: technically, it says the length in characters, but I think this one might be correct // Technically, it says the length in characters, but I think this one might be correct.
// It doesn't really matter, as only ASCII characters are being used.
// see also Decode#varString() // see also Decode#varString()
varInt(bytes.length, stream); varInt(bytes.length, out);
stream.write(bytes); out.write(bytes);
}
public static void varBytes(byte[] data, OutputStream out) throws IOException {
varInt(data.length, out);
out.write(data);
} }
/** /**
* Serializes a {@link Streamable} object and returns the byte array.
*
* @param streamable the object to be serialized * @param streamable the object to be serialized
* @return an array of bytes representing the given streamable object. * @return an array of bytes representing the given streamable object.
* @throws IOException if an I/O error occurs. * @throws IOException if an I/O error occurs.

View File

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

View File

@ -16,6 +16,9 @@
package ch.dissem.bitmessage.utils; package ch.dissem.bitmessage.utils;
import java.util.Arrays;
import java.util.Objects;
/** /**
* Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now * Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now
* used to contain different status information. It is by default displayed in some JSON inspired human readable * used to contain different status information. It is by default displayed in some JSON inspired human readable
@ -43,12 +46,19 @@ public class Property {
return value; return value;
} }
public Property getProperty(String name) { /**
* Returns the property if available or <code>null</code> otherwise.
* Subproperties can be requested by submitting the sequence of properties.
*/
public Property getProperty(String... name) {
if (name == null || name.length == 0) return null;
for (Property p : properties) { for (Property p : properties) {
if (name == null) { if (Objects.equals(name[0], p.name)) {
if (p.name == null) return p; if (name.length == 1)
} else { return p;
if (name.equals(p.name)) return p; else
return p.getProperty(Arrays.copyOfRange(name, 1, name.length));
} }
} }
return null; return null;

View File

@ -16,23 +16,21 @@
package ch.dissem.bitmessage.utils; package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.ports.Security; import ch.dissem.bitmessage.ports.Cryptography;
/** /**
* Created by chris on 20.07.15. * @author Christian Basler
*/ */
public class Singleton { public class Singleton {
private static Security security; private static Cryptography cryptography;
public static void initialize(Security security) { public static void initialize(Cryptography cryptography) {
synchronized (Singleton.class) { synchronized (Singleton.class) {
if (Singleton.security == null) { Singleton.cryptography = cryptography;
Singleton.security = security;
}
} }
} }
public static Security security() { public static Cryptography security() {
return security; return cryptography;
} }
} }

View File

@ -16,8 +16,6 @@
package ch.dissem.bitmessage.utils; package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.payload.ObjectType;
/** /**
* Some utilities to handle strings. * Some utilities to handle strings.
* TODO: Probably this should be split in a GUI related and an SQL related utility class. * TODO: Probably this should be split in a GUI related and an SQL related utility class.

View File

@ -0,0 +1,40 @@
package ch.dissem.bitmessage.utils;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
/**
* Stores times to live for different object types. Usually this shouldn't be messed with,
* but for tests it might be a good idea to reduce it to a minimum, and on mobile clients
* you might want to optimize it as well.
*
* @author Christian Basler
*/
public class TTL {
private static long msg = 2 * DAY;
private static long getpubkey = 2 * DAY;
private static long pubkey = 28 * DAY;
public static long msg() {
return msg;
}
public static void msg(long msg) {
TTL.msg = msg;
}
public static long getpubkey() {
return getpubkey;
}
public static void getpubkey(long getpubkey) {
TTL.getpubkey = getpubkey;
}
public static long pubkey() {
return pubkey;
}
public static void pubkey(long pubkey) {
TTL.pubkey = pubkey;
}
}

View File

@ -0,0 +1,257 @@
/*
* 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.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.MessageMatchers;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TestUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Test;
import java.util.*;
import static ch.dissem.bitmessage.entity.payload.ObjectType.*;
import static ch.dissem.bitmessage.utils.MessageMatchers.object;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* @author Christian Basler
*/
public class BitmessageContextTest {
private BitmessageContext ctx;
private BitmessageContext.Listener listener;
@Before
public void setUp() throws Exception {
Singleton.initialize(null);
listener = mock(BitmessageContext.Listener.class);
ctx = new BitmessageContext.Builder()
.addressRepo(mock(AddressRepository.class))
.cryptography(new BouncyCryptography())
.inventory(mock(Inventory.class))
.listener(listener)
.messageCallback(mock(MessageCallback.class))
.messageRepo(mock(MessageRepository.class))
.networkHandler(mock(NetworkHandler.class))
.nodeRegistry(mock(NodeRegistry.class))
.powRepo(mock(ProofOfWorkRepository.class))
.proofOfWorkEngine(mock(ProofOfWorkEngine.class))
.build();
}
@Test
public void ensureContactIsSavedAndPubkeyRequested() {
BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
ctx.addContact(contact);
verify(ctx.addresses(), times(2)).save(contact);
verify(ctx.internals().getProofOfWorkEngine())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensurePubkeyIsNotRequestedIfItExists() throws Exception {
ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload");
Pubkey pubkey = (Pubkey) object.getPayload();
BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
contact.setPubkey(pubkey);
ctx.addContact(contact);
verify(ctx.addresses(), times(1)).save(contact);
verify(ctx.internals().getProofOfWorkEngine(), never())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureV2PubkeyIsNotRequestedIfItExistsInInventory() throws Exception {
BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class)))
.thenReturn(Collections.singletonList(
TestUtils.loadObjectMessage(2, "V2Pubkey.payload")
));
ctx.addContact(contact);
verify(ctx.addresses(), atLeastOnce()).save(contact);
verify(ctx.internals().getProofOfWorkEngine(), never())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureV4PubkeyIsNotRequestedIfItExistsInInventory() throws Exception {
BitmessageAddress contact = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h");
when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class)))
.thenReturn(Collections.singletonList(
TestUtils.loadObjectMessage(2, "V4Pubkey.payload")
));
final BitmessageAddress stored = new BitmessageAddress(contact.getAddress());
stored.setAlias("Test");
when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(stored);
ctx.addContact(contact);
verify(ctx.addresses(), atLeastOnce()).save(argThat(new BaseMatcher<BitmessageAddress>() {
@Override
public boolean matches(Object item) {
return item instanceof BitmessageAddress
&& ((BitmessageAddress) item).getPubkey() != null
&& stored.getAlias().equals(((BitmessageAddress) item).getAlias());
}
@Override
public void describeTo(Description description) {
description.appendText("pubkey must not be null and alias must be ").appendValue(stored.getAlias());
}
}));
verify(ctx.internals().getProofOfWorkEngine(), never())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureSubscriptionIsAddedAndExistingBroadcastsRetrieved() throws Exception {
BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ");
List<ObjectMessage> objects = new LinkedList<>();
objects.add(TestUtils.loadObjectMessage(4, "V4Broadcast.payload"));
objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload"));
when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)))
.thenReturn(objects);
when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address));
ctx.addSubscribtion(address);
verify(ctx.addresses(), atLeastOnce()).save(address);
assertThat(address.isSubscribed(), is(true));
verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class));
verify(listener).receive(any(Plaintext.class));
}
@Test
public void ensureIdentityIsCreated() {
assertThat(ctx.createIdentity(false), notNullValue());
}
@Test
public void ensureMessageIsSent() throws Exception {
ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(),
"Subject", "Message");
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(MSG), eq(1000L), eq(1000L));
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Plaintext.Type.MSG));
}
@Test
public void ensurePubkeyIsRequestedIfItIsMissing() throws Exception {
ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"),
new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"),
"Subject", "Message");
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(GET_PUBKEY), eq(1000L), eq(1000L));
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Plaintext.Type.MSG));
}
@Test(expected = IllegalArgumentException.class)
public void ensureSenderMustBeIdentity() {
ctx.send(new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"),
new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"),
"Subject", "Message");
}
@Test
public void ensureBroadcastIsSent() throws Exception {
ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"),
"Subject", "Message");
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(BROADCAST), eq(1000L), eq(1000L));
verify(ctx.internals().getProofOfWorkEngine())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
verify(ctx.messages(), timeout(10000).atLeastOnce())
.save(MessageMatchers.plaintext(Plaintext.Type.BROADCAST));
}
@Test(expected = IllegalArgumentException.class)
public void ensureSenderWithoutPrivateKeyThrowsException() {
Plaintext msg = new Plaintext.Builder(Plaintext.Type.BROADCAST)
.from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.message("Subject", "Message")
.build();
ctx.send(msg);
}
@Test
public void ensureChanIsJoined() {
String chanAddress = "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r";
BitmessageAddress chan = ctx.joinChan("general", chanAddress);
assertNotNull(chan);
assertEquals(chan.getAddress(), chanAddress);
assertTrue(chan.isChan());
}
@Test
public void ensureDeterministicAddressesAreCreated() {
final int expected_size = 8;
List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, false);
assertEquals(expected_size, addresses.size());
Set<String> expected = new HashSet<>(expected_size);
expected.add("BM-2cWFkyuXXFw6d393RGnin2RpSXj8wxtt6F");
expected.add("BM-2cX8TF9vuQZEWvT7UrEeq1HN9dgiSUPLEN");
expected.add("BM-2cUzX8f9CKUU7L8NeB8GExZvf54PrcXq1S");
expected.add("BM-2cU7MAoQd7KE8SPF7AKFPpoEZKjk86KRqE");
expected.add("BM-2cVm8ByVBacc2DVhdTNs6rmy5ZQK6DUsrt");
expected.add("BM-2cW2af1vB6kWon2WkygDHqGwfcpfAFm2Jk");
expected.add("BM-2cWdWD7UtUN4gWChgNX9pvyvNPjUZvU8BT");
expected.add("BM-2cXkYgYcUrv4fGxSHzyEScW955Cc8sDteo");
for (BitmessageAddress a : addresses) {
assertTrue(expected.contains(a.getAddress()));
expected.remove(a.getAddress());
}
}
@Test
public void ensureShortDeterministicAddressesAreCreated() {
final int expected_size = 1;
List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, true);
assertEquals(expected_size, addresses.size());
Set<String> expected = new HashSet<>(expected_size);
expected.add("BM-NBGyBAEp6VnBkFWKpzUSgxuTqVdWPi78");
for (BitmessageAddress a : addresses) {
assertTrue(expected.contains(a.getAddress()));
expected.remove(a.getAddress());
}
}
@Test
public void ensureChanIsCreated() {
BitmessageAddress chan = ctx.createChan("test");
assertNotNull(chan);
assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION);
assertTrue(chan.isChan());
}
}

View File

@ -0,0 +1,149 @@
/*
* 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.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.GetPubkey;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.ports.Labeler;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.MessageMatchers.plaintext;
import static org.mockito.Mockito.*;
/**
* @author Christian Basler
*/
public class DefaultMessageListenerTest extends TestBase {
@Mock
private AddressRepository addressRepo;
@Mock
private MessageRepository messageRepo;
private InternalContext ctx;
private DefaultMessageListener listener;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
ctx = mock(InternalContext.class);
Singleton.initialize(new BouncyCryptography());
when(ctx.getAddressRepository()).thenReturn(addressRepo);
when(ctx.getMessageRepository()).thenReturn(messageRepo);
listener = new DefaultMessageListener(ctx, mock(Labeler.class), mock(BitmessageContext.Listener.class));
}
@Test
public void ensurePubkeyIsSentOnRequest() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
when(addressRepo.findIdentity(any(byte[].class)))
.thenReturn(identity);
listener.receive(new ObjectMessage.Builder()
.stream(2)
.payload(new GetPubkey(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")))
.build());
verify(ctx).sendPubkey(eq(identity), eq(2L));
}
@Test
public void ensureIncomingPubkeyIsAddedToContact() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress contact = new BitmessageAddress(identity.getAddress());
when(addressRepo.findContact(any(byte[].class)))
.thenReturn(contact);
when(messageRepo.findMessages(eq(PUBKEY_REQUESTED), eq(contact)))
.thenReturn(Collections.singletonList(
new Plaintext.Builder(MSG).from(identity).to(contact).message("S", "T").build()
));
ObjectMessage objectMessage = new ObjectMessage.Builder()
.stream(2)
.payload(identity.getPubkey())
.build();
objectMessage.sign(identity.getPrivateKey());
objectMessage.encrypt(Singleton.security().createPublicKey(identity.getPublicDecryptionKey()));
listener.receive(objectMessage);
verify(addressRepo).save(any(BitmessageAddress.class));
}
@Test
public void ensureIncomingMessageIsSaved() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress contact = new BitmessageAddress(identity.getAddress());
when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity));
ObjectMessage objectMessage = new ObjectMessage.Builder()
.stream(2)
.payload(new Msg(new Plaintext.Builder(MSG)
.from(identity)
.to(contact)
.message("S", "T")
.build()))
.nonce(new byte[8])
.build();
objectMessage.sign(identity.getPrivateKey());
objectMessage.encrypt(identity.getPubkey());
listener.receive(objectMessage);
verify(messageRepo, atLeastOnce()).save(plaintext(MSG));
}
@Test
public void ensureIncomingBroadcastIsSaved() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
when(addressRepo.getSubscriptions(anyLong())).thenReturn(Collections.singletonList(identity));
Broadcast broadcast = Factory.getBroadcast(new Plaintext.Builder(BROADCAST)
.from(identity)
.message("S", "T")
.build());
ObjectMessage objectMessage = new ObjectMessage.Builder()
.stream(2)
.payload(broadcast)
.nonce(new byte[8])
.build();
objectMessage.sign(identity.getPrivateKey());
broadcast.encrypt();
listener.receive(objectMessage);
verify(messageRepo, atLeastOnce()).save(plaintext(BROADCAST));
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
/**
* @author Christian Basler
*/
public class ProofOfWorkServiceTest {
private ProofOfWorkService proofOfWorkService;
private Cryptography cryptography;
@Mock
private InternalContext ctx;
@Mock
private ProofOfWorkRepository proofOfWorkRepo;
@Mock
private Inventory inventory;
@Mock
private NetworkHandler networkHandler;
@Mock
private MessageRepository messageRepo;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
cryptography = spy(new BouncyCryptography());
Singleton.initialize(cryptography);
ctx = mock(InternalContext.class);
when(ctx.getProofOfWorkRepository()).thenReturn(proofOfWorkRepo);
when(ctx.getInventory()).thenReturn(inventory);
when(ctx.getNetworkHandler()).thenReturn(networkHandler);
when(ctx.getMessageRepository()).thenReturn(messageRepo);
proofOfWorkService = new ProofOfWorkService();
proofOfWorkService.setContext(ctx);
}
@Test
public void ensureMissingProofOfWorkIsDone() {
when(proofOfWorkRepo.getItems()).thenReturn(Arrays.asList(new byte[64]));
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));
proofOfWorkService.doMissingProofOfWork();
verify(cryptography).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L),
any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureCalculatedNonceIsStored() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress address = TestUtils.loadContact();
Plaintext plaintext = new Plaintext.Builder(MSG).from(identity).to(address).message("", "").build();
ObjectMessage object = new ObjectMessage.Builder()
.payload(new Msg(plaintext))
.build();
object.sign(identity.getPrivateKey());
object.encrypt(address.getPubkey());
byte[] initialHash = new byte[64];
byte[] nonce = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
when(proofOfWorkRepo.getItem(initialHash)).thenReturn(new ProofOfWorkRepository.Item(object, 1001, 1002));
when(messageRepo.getMessage(initialHash)).thenReturn(plaintext);
proofOfWorkService.onNonceCalculated(initialHash, nonce);
verify(proofOfWorkRepo).removeObject(eq(initialHash));
verify(inventory).storeObject(eq(object));
verify(networkHandler).offer(eq(object.getInventoryVector()));
assertThat(plaintext.getInventoryVector(), equalTo(object.getInventoryVector()));
}
}

View File

@ -22,7 +22,6 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Msg;
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.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.TestBase; import ch.dissem.bitmessage.utils.TestBase;
@ -30,7 +29,6 @@ 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.Date;
import static org.junit.Assert.*; import static org.junit.Assert.*;

View File

@ -20,7 +20,10 @@ 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.*; import ch.dissem.bitmessage.utils.Base58;
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;
@ -55,44 +58,56 @@ public class BitmessageAddressTest {
} }
@Test @Test
public void testCreateAddress() { 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());
} }
@Test @Test
public void testV2PubkeyImport() throws IOException { public void ensureV2PubkeyCanBeImported() throws IOException {
ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload");
Pubkey pubkey = (Pubkey) object.getPayload(); Pubkey pubkey = (Pubkey) object.getPayload();
BitmessageAddress address = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); BitmessageAddress address = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
try {
address.setPubkey(pubkey); address.setPubkey(pubkey);
} catch (Exception e) {
fail(e.getMessage());
}
} }
@Test @Test
public void testV3PubkeyImport() throws IOException { public void ensureV3PubkeyCanBeImported() throws IOException {
BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ");
assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe()); assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe());
ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload");
Pubkey pubkey = (Pubkey) object.getPayload(); Pubkey pubkey = (Pubkey) object.getPayload();
assertTrue(object.isSignatureValid(pubkey)); assertTrue(object.isSignatureValid(pubkey));
try {
address.setPubkey(pubkey); address.setPubkey(pubkey);
} catch (Exception e) {
fail(e.getMessage());
}
assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe());
} }
@Test @Test
public void testV4PubkeyImport() throws IOException, DecryptionFailedException { public void ensureV4PubkeyCanBeImported() 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(4, "V4Pubkey.payload");
object.decrypt(address.getPublicDecryptionKey()); object.decrypt(address.getPublicDecryptionKey());
V4Pubkey pubkey = (V4Pubkey) object.getPayload(); V4Pubkey pubkey = (V4Pubkey) object.getPayload();
assertTrue(object.isSignatureValid(pubkey)); assertTrue(object.isSignatureValid(pubkey));
try {
address.setPubkey(pubkey); address.setPubkey(pubkey);
} catch (Exception e) {
fail(e.getMessage());
}
} }
@Test @Test
public void testV3AddressImport() throws IOException { public void ensureV3IdentityCanBeImported() throws IOException {
String address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"; String address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn";
assertEquals(3, new BitmessageAddress(address_string).getVersion()); assertEquals(3, new BitmessageAddress(address_string).getVersion());
assertEquals(1, new BitmessageAddress(address_string).getStream()); assertEquals(1, new BitmessageAddress(address_string).getStream());
@ -108,9 +123,17 @@ public class BitmessageAddressTest {
} }
@Test @Test
public void testGetSecret() throws IOException { public void ensureV4IdentityCanBeImported() throws IOException {
assertHexEquals("0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D", assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion());
getSecret("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ")); byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU");
byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress());
}
private void assertHexEquals(String hex, byte[] bytes) {
assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase());
} }
private byte[] getSecret(String walletImportFormat) throws IOException { private byte[] getSecret(String walletImportFormat) throws IOException {
@ -126,18 +149,4 @@ public class BitmessageAddressTest {
} }
return Arrays.copyOfRange(bytes, 1, 33); return Arrays.copyOfRange(bytes, 1, 33);
} }
@Test
public void testV4AddressImport() throws IOException {
assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion());
byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU");
byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress());
}
private void assertHexEquals(String hex, byte[] bytes) {
assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase());
}
} }

View File

@ -17,6 +17,7 @@
package ch.dissem.bitmessage.entity; package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.payload.*;
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.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestBase;
@ -25,9 +26,11 @@ import org.junit.Test;
import java.io.*; import java.io.*;
import java.lang.reflect.Field; import java.lang.reflect.Field;
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 org.junit.Assert.*; import static org.junit.Assert.*;
public class SerializationTest extends TestBase { public class SerializationTest extends TestBase {
@ -95,6 +98,24 @@ public class SerializationTest extends TestBase {
assertEquals(p1, p2); assertEquals(p1, p2);
} }
@Test
public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception {
ArrayList<InventoryVector> ivs = new ArrayList<>(50000);
for (int i = 0; i < 50000; i++) {
ivs.add(new InventoryVector(security().randomBytes(32)));
}
Inv inv = new Inv.Builder().inventory(ivs).build();
NetworkMessage before = new NetworkMessage(inv);
ByteArrayOutputStream out = new ByteArrayOutputStream();
before.write(out);
NetworkMessage after = Factory.getNetworkMessage(3, new ByteArrayInputStream(out.toByteArray()));
assertNotNull(after);
Inv invAfter = (Inv) after.getPayload();
assertEquals(ivs, invAfter.getInventory());
}
private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException { private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException {
byte[] data = TestUtils.getBytes(resourceName); byte[] data = TestUtils.getBytes(resourceName);
InputStream in = new ByteArrayInputStream(data); InputStream in = new ByteArrayInputStream(data);

View File

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

View File

@ -57,7 +57,7 @@ public class EncodeTest {
checkBytes(stream, 4, 3, 2, 1); checkBytes(stream, 4, 3, 2, 1);
stream = new ByteArrayOutputStream(); stream = new ByteArrayOutputStream();
Encode.int32(3355443201l, stream); Encode.int32(3355443201L, stream);
checkBytes(stream, 200, 0, 0, 1); checkBytes(stream, 200, 0, 0, 1);
} }

View File

@ -0,0 +1,57 @@
/*
* 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.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.mockito.Matchers;
/**
* @author Christian Basler
*/
public class MessageMatchers {
public static Plaintext plaintext(final Plaintext.Type type) {
return Matchers.argThat(new BaseMatcher<Plaintext>() {
@Override
public boolean matches(Object item) {
return item instanceof Plaintext && ((Plaintext) item).getType() == type;
}
@Override
public void describeTo(Description description) {
description.appendText("type should be ").appendValue(type);
}
});
}
public static ObjectMessage object(final ObjectType type) {
return Matchers.argThat(new BaseMatcher<ObjectMessage>() {
@Override
public boolean matches(Object item) {
return item instanceof ObjectMessage && ((ObjectMessage) item).getPayload().getType() == type;
}
@Override
public void describeTo(Description description) {
description.appendText("payload type should be ").appendValue(type);
}
});
}
}

View File

@ -16,13 +16,15 @@
package ch.dissem.bitmessage.utils; package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.security.bc.BouncySecurity; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography;
import org.junit.BeforeClass;
/** /**
* Created by chris on 20.07.15. * @author Christian Basler
*/ */
public class TestBase { public class TestBase {
static { @BeforeClass
Singleton.initialize(new BouncySecurity()); public static void setUpClass() {
Singleton.initialize(new BouncyCryptography());
} }
} }

Some files were not shown because too many files have changed in this diff Show More