Merge branch 'release/1.0.0'

This commit is contained in:
Christian Basler 2016-01-23 17:22:03 +01:00
commit e56d8c25e0
153 changed files with 5392 additions and 1913 deletions

View File

@ -1,2 +1,3 @@
language: java
jdk:
- oraclejdk8

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

@ -29,18 +29,21 @@ Setup
Add Jabit as Gradle dependency:
```Gradle
compile 'ch.dissem.jabit:jabit-domain:0.2.0'
compile 'ch.dissem.jabit:jabit-core:0.2.0'
```
Unless you want to implement your own, also add the following:
```Gradle
compile 'ch.dissem.jabit:jabit-networking:0.2.0'
compile 'ch.dissem.jabit:jabit-repositories:0.2.0'
compile 'ch.dissem.jabit:jabit-cryptography-bc:0.2.0'
```
And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add:
```Gradle
compile 'ch.dissem.jabit:jabit-wif:0.2.0'
```
For Android clients use `jabit-cryptography-sc` instead of `jabit-cryptography-bc`.
Usage
-----
@ -53,6 +56,7 @@ BitmessageContext ctx = new BitmessageContext.Builder()
.messageRepo(new JdbcMessageRepository(jdbcConfig))
.nodeRegistry(new MemoryNodeRegistry())
.networkHandler(new NetworkNode())
.cryptography(new BouncyCryptography())
.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

View File

@ -5,7 +5,7 @@ subprojects {
sourceCompatibility = 1.7
group = 'ch.dissem.jabit'
version = '0.2.0'
version = '1.0.0'
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
@ -13,6 +13,12 @@ subprojects {
mavenCentral()
}
test {
testLogging {
exceptionFormat = 'full'
}
}
task javadocJar(type: Jar) {
classifier = 'javadoc'
from javadoc
@ -37,11 +43,6 @@ subprojects {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
if (!hasProperty('ossrhUsername')) {
ext.ossrhUsername = 'dummy'
ext.ossrhPassword = 'dummy'
}
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}

31
core/build.gradle Normal file
View File

@ -0,0 +1,31 @@
uploadArchives {
repositories {
mavenDeployer {
pom.project {
name 'Jabit Core'
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.'
}
}
}
}
configurations {
testArtifacts.extendsFrom testRuntime
}
task testJar(type: Jar) {
classifier = 'test'
from sourceSets.test.output
}
artifacts {
testArtifacts testJar
}
dependencies {
compile 'org.slf4j:slf4j-api:1.7.12'
testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile project(':cryptography-bc')
}

View File

@ -0,0 +1,515 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.*;
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.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
import static ch.dissem.bitmessage.entity.Plaintext.Status.*;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
import static ch.dissem.bitmessage.utils.UnixTime.HOUR;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
/**
* <p>Use this class if you want to create a Bitmessage client.</p>
* You'll need the Builder to create a BitmessageContext, and set the following properties:
* <ul>
* <li>addressRepo</li>
* <li>inventory</li>
* <li>nodeRegistry</li>
* <li>networkHandler</li>
* <li>messageRepo</li>
* <li>streams</li>
* </ul>
* <p>The default implementations in the different module builds can be used.</p>
* <p>The port defaults to 8444 (the default Bitmessage port)</p>
*/
public class BitmessageContext {
public static final int CURRENT_VERSION = 3;
private final static Logger LOG = LoggerFactory.getLogger(BitmessageContext.class);
private final ExecutorService pool;
private final InternalContext ctx;
private final Listener listener;
private final NetworkHandler.MessageListener networkListener;
private final boolean sendPubkeyOnIdentityCreation;
private BitmessageContext(Builder builder) {
ctx = new InternalContext(builder);
listener = builder.listener;
networkListener = new DefaultMessageListener(ctx, listener);
// As this thread is used for parts that do POW, which itself uses parallel threads, only
// one should be executed at any time.
pool = Executors.newFixedThreadPool(1);
sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
ctx.getProofOfWorkService().doMissingProofOfWork();
}
}, 30_000); // After 30 seconds
}
public AddressRepository addresses() {
return ctx.getAddressRepository();
}
public MessageRepository messages() {
return ctx.getMessageRepository();
}
public BitmessageAddress createIdentity(boolean shorter, Feature... features) {
final BitmessageAddress identity = new BitmessageAddress(new PrivateKey(
shorter,
ctx.getStreams()[0],
ctx.getNetworkNonceTrialsPerByte(),
ctx.getNetworkExtraBytes(),
features
));
ctx.getAddressRepository().save(identity);
if (sendPubkeyOnIdentityCreation) {
pool.submit(new Runnable() {
@Override
public void run() {
ctx.sendPubkey(identity, identity.getStream());
}
});
}
return identity;
}
public void addDistributedMailingList(String address, String alias) {
// TODO
throw new RuntimeException("not implemented");
}
public void broadcast(final BitmessageAddress from, final String subject, final String message) {
pool.submit(new Runnable() {
@Override
public void run() {
Plaintext msg = new Plaintext.Builder(BROADCAST)
.from(from)
.message(subject, message)
.build();
LOG.info("Sending message.");
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
from,
from,
Factory.getBroadcast(from, msg),
+2 * DAY
);
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) {
if (from.getPrivateKey() == null) {
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)
.from(from)
.to(to)
.message(subject, message)
.labels(messages().getLabels(Label.Type.SENT))
.build();
if (to.getPubkey() == null) {
tryToFindMatchingPubkey(to);
}
if (to.getPubkey() == null) {
LOG.info("Public key is missing from recipient. Requesting.");
requestPubkey(from, to);
msg.setStatus(PUBKEY_REQUESTED);
ctx.getMessageRepository().save(msg);
} else {
LOG.info("Sending message.");
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
from,
to,
new Msg(msg),
+2 * DAY
);
msg.setStatus(SENT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT));
ctx.getMessageRepository().save(msg);
}
}
});
}
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.getPubkey() == null) {
tryToFindMatchingPubkey(to);
}
if (to.getPubkey() == null) {
LOG.info("Public key is missing from recipient. Requesting.");
requestPubkey(msg.getFrom(), to);
msg.setStatus(PUBKEY_REQUESTED);
ctx.getMessageRepository().save(msg);
} else {
LOG.info("Sending message.");
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
msg.getFrom(),
to,
new Msg(msg),
+2 * DAY
);
msg.setStatus(SENT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT));
ctx.getMessageRepository().save(msg);
}
}
});
}
private void requestPubkey(BitmessageAddress requestingIdentity, BitmessageAddress address) {
ctx.send(
requestingIdentity,
address,
new GetPubkey(address),
+28 * DAY
);
}
public void startup() {
ctx.getNetworkHandler().start(networkListener);
}
public void shutdown() {
ctx.getNetworkHandler().stop();
}
/**
* @param host a trusted node that must be reliable (it's used for every synchronization)
* @param port of the trusted host, default is 8444
* @param timeoutInSeconds synchronization should end no later than about 5 seconds after the timeout elapsed, even
* if not all objects were fetched
* @param wait waits for the synchronization thread to finish
*/
public void synchronize(InetAddress host, int port, long timeoutInSeconds, boolean wait) {
Future<?> future = ctx.getNetworkHandler().synchronize(host, port, networkListener, timeoutInSeconds);
if (wait) {
try {
future.get();
} catch (InterruptedException e) {
LOG.info("Thread was interrupted. Trying to shut down synchronization and returning.");
future.cancel(true);
} catch (CancellationException | ExecutionException e) {
LOG.debug(e.getMessage(), e);
}
}
}
/**
* Send a custom message to a specific node (that should implement handling for this message type) and returns
* the response, which in turn is expected to be a {@link CustomMessage}.
*
* @param server the node's address
* @param port the node's port
* @param request the request
* @return the response
*/
public CustomMessage send(InetAddress server, int port, CustomMessage request) {
return ctx.getNetworkHandler().send(server, port, request);
}
public void cleanup() {
ctx.getInventory().cleanup();
}
public boolean isRunning() {
return ctx.getNetworkHandler().isRunning();
}
public void addContact(BitmessageAddress contact) {
ctx.getAddressRepository().save(contact);
tryToFindMatchingPubkey(contact);
if (contact.getPubkey() == null) {
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.getAddressRepository().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.getAddressRepository().save(address);
break;
}
}
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
}
}
public void addSubscribtion(BitmessageAddress address) {
address.setSubscribed(true);
ctx.getAddressRepository().save(address);
tryToFindBroadcastsForAddress(address);
}
private void tryToFindBroadcastsForAddress(BitmessageAddress address) {
for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), Broadcast.getVersion(address), ObjectType.BROADCAST)) {
try {
Broadcast broadcast = (Broadcast) object.getPayload();
broadcast.decrypt(address);
listener.receive(broadcast.getPlaintext());
} catch (DecryptionFailedException ignore) {
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
}
}
public Property status() {
return new Property("status", null,
ctx.getNetworkHandler().getNetworkStatus()
);
}
/**
* Returns the {@link InternalContext} - normally you wouldn't need it,
* unless you are doing something crazy with the protocol.
*/
public InternalContext internals() {
return ctx;
}
public interface Listener {
void receive(Plaintext plaintext);
}
public static final class Builder {
int port = 8444;
Inventory inventory;
NodeRegistry nodeRegistry;
NetworkHandler networkHandler;
AddressRepository addressRepo;
MessageRepository messageRepo;
ProofOfWorkRepository proofOfWorkRepository;
ProofOfWorkEngine proofOfWorkEngine;
Cryptography cryptography;
MessageCallback messageCallback;
CustomCommandHandler customCommandHandler;
Listener listener;
int connectionLimit = 150;
long connectionTTL = 30 * MINUTE;
boolean sendPubkeyOnIdentityCreation = true;
long pubkeyTTL = 28;
public Builder() {
}
public Builder port(int port) {
this.port = port;
return this;
}
public Builder inventory(Inventory inventory) {
this.inventory = inventory;
return this;
}
public Builder nodeRegistry(NodeRegistry nodeRegistry) {
this.nodeRegistry = nodeRegistry;
return this;
}
public Builder networkHandler(NetworkHandler networkHandler) {
this.networkHandler = networkHandler;
return this;
}
public Builder addressRepo(AddressRepository addressRepo) {
this.addressRepo = addressRepo;
return this;
}
public Builder messageRepo(MessageRepository messageRepo) {
this.messageRepo = messageRepo;
return this;
}
public Builder powRepo(ProofOfWorkRepository proofOfWorkRepository) {
this.proofOfWorkRepository = proofOfWorkRepository;
return this;
}
public Builder cryptography(Cryptography cryptography) {
this.cryptography = cryptography;
return this;
}
public Builder messageCallback(MessageCallback callback) {
this.messageCallback = callback;
return this;
}
public Builder customCommandHandler(CustomCommandHandler handler) {
this.customCommandHandler = handler;
return this;
}
public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) {
this.proofOfWorkEngine = proofOfWorkEngine;
return this;
}
public Builder listener(Listener listener) {
this.listener = listener;
return this;
}
public Builder connectionLimit(int connectionLimit) {
this.connectionLimit = connectionLimit;
return this;
}
public Builder connectionTTL(int hours) {
this.connectionTTL = hours * HOUR;
return this;
}
/**
* By default a client will send the public key when an identity is being created. On weaker devices
* this behaviour might not be desirable.
*/
public Builder doNotSendPubkeyOnIdentityCreation() {
this.sendPubkeyOnIdentityCreation = false;
return this;
}
/**
* Time to live in seconds for public keys the client sends. Defaults to the maximum of 28 days,
* but on weak devices smaller values might be desirable.
* <p>
* Please be aware that this might cause some problems where you can't receive a message (the
* sender can't receive your public key) in some special situations. Also note that it's probably
* not a good idea to set it too low.
* </p>
*/
public Builder pubkeyTTL(long days) {
if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days");
this.pubkeyTTL = days;
return this;
}
public BitmessageContext build() {
nonNull("inventory", inventory);
nonNull("nodeRegistry", nodeRegistry);
nonNull("networkHandler", networkHandler);
nonNull("addressRepo", addressRepo);
nonNull("messageRepo", messageRepo);
nonNull("proofOfWorkRepo", proofOfWorkRepository);
if (proofOfWorkEngine == null) {
proofOfWorkEngine = new MultiThreadedPOWEngine();
}
if (messageCallback == null) {
messageCallback = new MessageCallback() {
@Override
public void proofOfWorkStarted(ObjectPayload message) {
}
@Override
public void proofOfWorkCompleted(ObjectPayload message) {
}
@Override
public void messageOffered(ObjectPayload message, InventoryVector iv) {
}
@Override
public void messageAcknowledged(InventoryVector iv) {
}
};
}
if (customCommandHandler == null) {
customCommandHandler = new CustomCommandHandler() {
@Override
public MessagePayload handle(CustomMessage request) {
throw new RuntimeException("Received custom request, but no custom command handler configured.");
}
};
}
return new BitmessageContext(this);
}
private void nonNull(String name, Object o) {
if (o == null) throw new IllegalStateException(name + " must not be null");
}
}
}

View File

@ -69,9 +69,10 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
}
protected void receive(ObjectMessage object, GetPubkey getPubkey) {
BitmessageAddress identity = ctx.getAddressRepo().findIdentity(getPubkey.getRipeTag());
BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag());
if (identity != null && identity.getPrivateKey() != null) {
LOG.debug("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
ctx.sendPubkey(identity, object.getStream());
}
}
@ -81,40 +82,42 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
try {
if (pubkey instanceof V4Pubkey) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
address = ctx.getAddressRepo().findContact(v4Pubkey.getTag());
address = ctx.getAddressRepository().findContact(v4Pubkey.getTag());
if (address != null) {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
}
} else {
address = ctx.getAddressRepo().findContact(pubkey.getRipe());
address = ctx.getAddressRepository().findContact(pubkey.getRipe());
}
if (address != null) {
address.setPubkey(pubkey);
LOG.debug("Got pubkey for contact " + address);
ctx.getAddressRepo().save(address);
List<Plaintext> messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address);
LOG.debug("Sending " + messages.size() + " messages for contact " + address);
for (Plaintext msg : messages) {
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
msg.getFrom(),
msg.getTo(),
new Msg(msg),
+2 * DAY,
ctx.getNonceTrialsPerByte(msg.getTo()),
ctx.getExtraBytes(msg.getTo())
);
msg.setStatus(SENT);
ctx.getMessageRepository().save(msg);
}
updatePubkey(address, pubkey);
}
} catch (DecryptionFailedException ignore) {
}
}
private void updatePubkey(BitmessageAddress address, Pubkey pubkey){
address.setPubkey(pubkey);
LOG.info("Got pubkey for contact " + address);
ctx.getAddressRepository().save(address);
List<Plaintext> messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address);
LOG.info("Sending " + messages.size() + " messages for contact " + address);
for (Plaintext msg : messages) {
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
msg.getFrom(),
msg.getTo(),
new Msg(msg),
+2 * DAY
);
msg.setStatus(SENT);
ctx.getMessageRepository().save(msg);
}
}
protected void receive(ObjectMessage object, Msg msg) throws IOException {
for (BitmessageAddress identity : ctx.getAddressRepo().getIdentities()) {
for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) {
try {
msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey());
msg.getPlaintext().setTo(identity);
@ -126,6 +129,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
msg.getPlaintext().setInventoryVector(object.getInventoryVector());
ctx.getMessageRepository().save(msg.getPlaintext());
listener.receive(msg.getPlaintext());
updatePubkey(msg.getPlaintext().getFrom(), msg.getPlaintext().getFrom().getPubkey());
}
break;
} catch (DecryptionFailedException ignore) {
@ -135,7 +139,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException {
byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null;
for (BitmessageAddress subscription : ctx.getAddressRepo().getSubscriptions(broadcast.getVersion())) {
for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) {
if (tag != null && !Arrays.equals(tag, subscription.getTag())) {
continue;
}
@ -149,6 +153,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
broadcast.getPlaintext().setInventoryVector(object.getInventoryVector());
ctx.getMessageRepository().save(broadcast.getPlaintext());
listener.receive(broadcast.getPlaintext());
updatePubkey(broadcast.getPlaintext().getFrom(), broadcast.getPlaintext().getFrom().getPubkey());
}
} catch (DecryptionFailedException ignore) {
}

View File

@ -16,12 +16,14 @@
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.GetPubkey;
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Security;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,8 +31,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.TreeSet;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
/**
* The internal context should normally only be used for port implementations. If you need it in your client
* implementation, you're either doing something wrong, something very weird, or the BitmessageContext should
@ -42,32 +42,64 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY;
public class InternalContext {
private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class);
private final Cryptography cryptography;
private final Inventory inventory;
private final NodeRegistry nodeRegistry;
private final NetworkHandler networkHandler;
private final AddressRepository addressRepository;
private final MessageRepository messageRepository;
private final ProofOfWorkRepository proofOfWorkRepository;
private final ProofOfWorkEngine proofOfWorkEngine;
private final MessageCallback messageCallback;
private final CustomCommandHandler customCommandHandler;
private final ProofOfWorkService proofOfWorkService;
private final TreeSet<Long> streams = new TreeSet<>();
private final int port;
private long networkNonceTrialsPerByte = 1000;
private long networkExtraBytes = 1000;
private long clientNonce;
private final long clientNonce;
private final long networkNonceTrialsPerByte = 1000;
private final long networkExtraBytes = 1000;
private final long pubkeyTTL;
private long connectionTTL;
private int connectionLimit;
public InternalContext(BitmessageContext.Builder builder) {
this.cryptography = builder.cryptography;
this.inventory = builder.inventory;
this.nodeRegistry = builder.nodeRegistry;
this.networkHandler = builder.networkHandler;
this.addressRepository = builder.addressRepo;
this.messageRepository = builder.messageRepo;
this.proofOfWorkRepository = builder.proofOfWorkRepository;
this.proofOfWorkService = new ProofOfWorkService();
this.proofOfWorkEngine = builder.proofOfWorkEngine;
this.clientNonce = Security.randomNonce();
this.clientNonce = cryptography.randomNonce();
this.messageCallback = builder.messageCallback;
this.customCommandHandler = builder.customCommandHandler;
this.port = builder.port;
this.connectionLimit = builder.connectionLimit;
this.connectionTTL = builder.connectionTTL;
this.pubkeyTTL = builder.pubkeyTTL;
port = builder.port;
streams.add(1L); // FIXME
Singleton.initialize(cryptography);
init(inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, proofOfWorkEngine);
// TODO: streams of new identities and subscriptions should also be added. This works only after a restart.
for (BitmessageAddress address : addressRepository.getIdentities()) {
streams.add(address.getStream());
}
for (BitmessageAddress address : addressRepository.getSubscriptions()) {
streams.add(address.getStream());
}
if (streams.isEmpty()) {
streams.add(1L);
}
init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine,
messageCallback, customCommandHandler);
for (BitmessageAddress identity : addressRepository.getIdentities()) {
streams.add(identity.getStream());
}
}
private void init(Object... objects) {
@ -78,6 +110,10 @@ public class InternalContext {
}
}
public Cryptography getCryptography() {
return cryptography;
}
public Inventory getInventory() {
return inventory;
}
@ -90,7 +126,7 @@ public class InternalContext {
return networkHandler;
}
public AddressRepository getAddressRepo() {
public AddressRepository getAddressRepository() {
return addressRepository;
}
@ -98,10 +134,18 @@ public class InternalContext {
return messageRepository;
}
public ProofOfWorkRepository getProofOfWorkRepository() {
return proofOfWorkRepository;
}
public ProofOfWorkEngine getProofOfWorkEngine() {
return proofOfWorkEngine;
}
public ProofOfWorkService getProofOfWorkService() {
return proofOfWorkService;
}
public long[] getStreams() {
long[] result = new long[streams.size()];
int i = 0;
@ -111,14 +155,6 @@ public class InternalContext {
return result;
}
public void addStream(long stream) {
streams.add(stream);
}
public void removeStream(long stream) {
streams.remove(stream);
}
public int getPort() {
return port;
}
@ -127,26 +163,17 @@ public class InternalContext {
return networkNonceTrialsPerByte;
}
public long getNonceTrialsPerByte(BitmessageAddress address) {
long nonceTrialsPerByte = address.getPubkey().getNonceTrialsPerByte();
return networkNonceTrialsPerByte > nonceTrialsPerByte ? networkNonceTrialsPerByte : nonceTrialsPerByte;
}
public long getNetworkExtraBytes() {
return networkExtraBytes;
}
public long getExtraBytes(BitmessageAddress address) {
long extraBytes = address.getPubkey().getExtraBytes();
return networkExtraBytes > extraBytes ? networkExtraBytes : extraBytes;
}
public void send(BitmessageAddress from, BitmessageAddress to, ObjectPayload payload, long timeToLive, long nonceTrialsPerByte, long extraBytes) {
public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload,
final long timeToLive) {
try {
if (to == null) to = from;
long expires = UnixTime.now(+timeToLive);
LOG.info("Expires at " + expires);
ObjectMessage object = new ObjectMessage.Builder()
final ObjectMessage object = new ObjectMessage.Builder()
.stream(to.getStream())
.expiresTime(expires)
.payload(payload)
@ -159,64 +186,58 @@ public class InternalContext {
} else if (payload instanceof Encrypted) {
object.encrypt(to.getPubkey());
}
Security.doProofOfWork(object, proofOfWorkEngine, nonceTrialsPerByte, extraBytes);
if (payload instanceof PlaintextHolder) {
Plaintext plaintext = ((PlaintextHolder) payload).getPlaintext();
plaintext.setInventoryVector(object.getInventoryVector());
messageRepository.save(plaintext);
}
inventory.storeObject(object);
networkHandler.offer(object.getInventoryVector());
messageCallback.proofOfWorkStarted(payload);
proofOfWorkService.doProofOfWork(to, object);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void sendPubkey(BitmessageAddress identity, long targetStream) {
public void sendPubkey(final BitmessageAddress identity, final long targetStream) {
try {
long expires = UnixTime.now(+28 * DAY);
long expires = UnixTime.now(pubkeyTTL);
LOG.info("Expires at " + expires);
ObjectMessage response = new ObjectMessage.Builder()
final ObjectMessage response = new ObjectMessage.Builder()
.stream(targetStream)
.expiresTime(expires)
.payload(identity.getPubkey())
.build();
response.sign(identity.getPrivateKey());
response.encrypt(Security.createPublicKey(identity.getPublicDecryptionKey()).getEncoded(false));
Security.doProofOfWork(response, proofOfWorkEngine, networkNonceTrialsPerByte, networkExtraBytes);
if (response.isSigned()) {
response.sign(identity.getPrivateKey());
}
if (response instanceof Encrypted) {
response.encrypt(Security.createPublicKey(identity.getPublicDecryptionKey()).getEncoded(false));
}
inventory.storeObject(response);
networkHandler.offer(response.getInventoryVector());
// TODO: save that the pubkey was just sent, and on which stream!
response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey()));
messageCallback.proofOfWorkStarted(identity.getPubkey());
// TODO: remember that the pubkey is just about to be sent, and on which stream!
proofOfWorkService.doProofOfWork(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void requestPubkey(BitmessageAddress contact) {
long expires = UnixTime.now(+2 * DAY);
public void requestPubkey(final BitmessageAddress contact) {
long expires = UnixTime.now(+pubkeyTTL);
LOG.info("Expires at " + expires);
ObjectMessage response = new ObjectMessage.Builder()
final ObjectMessage response = new ObjectMessage.Builder()
.stream(contact.getStream())
.expiresTime(expires)
.payload(new GetPubkey(contact))
.build();
Security.doProofOfWork(response, proofOfWorkEngine, networkNonceTrialsPerByte, networkExtraBytes);
inventory.storeObject(response);
networkHandler.offer(response.getInventoryVector());
messageCallback.proofOfWorkStarted(response.getPayload());
proofOfWorkService.doProofOfWork(response);
}
public long getClientNonce() {
return clientNonce;
}
public void setClientNonce(long clientNonce) {
this.clientNonce = clientNonce;
public long getConnectionTTL() {
return connectionTTL;
}
public int getConnectionLimit() {
return connectionLimit;
}
public CustomCommandHandler getCustomCommandHandler() {
return customCommandHandler;
}
public interface ContextHolder {

View File

@ -0,0 +1,52 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
/**
* Callback for message sending events, mostly so the user can be notified when POW is done.
*/
public interface MessageCallback {
/**
* Called before calculation of proof of work begins.
*/
void proofOfWorkStarted(ObjectPayload message);
/**
* Called after calculation of proof of work finished.
*/
void proofOfWorkCompleted(ObjectPayload message);
/**
* Called once the message is offered to the network. Please note that this doesn't mean the message was sent,
* if the client is not connected to the network it's just stored in the inventory.
* <p>
* Also, please note that this is where the original payload as well as the {@link InventoryVector} of the sent
* message is available. If the callback needs the IV for some reason, it should be retrieved here. (Plaintext
* and Broadcast messages will have their IV property set automatically though.)
* </p>
*/
void messageOffered(ObjectPayload message, InventoryVector iv);
/**
* This isn't called yet, as ACK messages aren't being processed yet. Also, this is only relevant for Plaintext
* messages.
*/
void messageAcknowledged(InventoryVector iv);
}

View File

@ -0,0 +1,82 @@
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.ports.Cryptography;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* @author Christian Basler
*/
public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalContext.ContextHolder {
private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class);
private 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) {
long nonceTrialsPerByte = recipient == null ?
ctx.getNetworkNonceTrialsPerByte() : recipient.getPubkey().getNonceTrialsPerByte();
long extraBytes = recipient == null ?
ctx.getNetworkExtraBytes() : recipient.getPubkey().getExtraBytes();
powRepo.putObject(object, nonceTrialsPerByte, extraBytes);
if (object.getPayload() instanceof PlaintextHolder) {
Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext();
plaintext.setInitialHash(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);
// messageCallback.proofOfWorkCompleted(payload);
Plaintext plaintext = messageRepo.getMessage(initialHash);
if (plaintext != null) {
plaintext.setInventoryVector(object.getInventoryVector());
messageRepo.save(plaintext);
}
ctx.getInventory().storeObject(object);
ctx.getProofOfWorkRepository().removeObject(initialHash);
ctx.getNetworkHandler().offer(object.getInventoryVector());
// messageCallback.messageOffered(payload, object.getInventoryVector());
}
@Override
public void setContext(InternalContext ctx) {
this.ctx = ctx;
this.cryptography = security();
this.powRepo = ctx.getProofOfWorkRepository();
this.messageRepo = ctx.getMessageRepository();
}
}

View File

@ -19,22 +19,27 @@ package ch.dissem.bitmessage.entity;
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.utils.*;
import ch.dissem.bitmessage.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Base58;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Encode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import static ch.dissem.bitmessage.utils.Decode.bytes;
import static ch.dissem.bitmessage.utils.Decode.varInt;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address
* holding private keys.
*/
public class BitmessageAddress {
public class BitmessageAddress implements Serializable {
private final long version;
private final long stream;
private final byte[] ripe;
@ -62,19 +67,19 @@ public class BitmessageAddress {
Encode.varInt(version, os);
Encode.varInt(stream, os);
if (version < 4) {
byte[] checksum = Security.sha512(os.toByteArray(), ripe);
byte[] checksum = security().sha512(os.toByteArray(), ripe);
this.tag = null;
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} else {
// for tag and decryption key, the checksum has to be created with 0x00 padding
byte[] checksum = Security.doubleSha512(os.toByteArray(), ripe);
byte[] checksum = security().doubleSha512(os.toByteArray(), ripe);
this.tag = Arrays.copyOfRange(checksum, 32, 64);
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
}
// but for the address and its checksum they need to be stripped
int offset = Bytes.numberOfLeadingZeros(ripe);
os.write(ripe, offset, ripe.length - offset);
byte[] checksum = Security.doubleSha512(os.toByteArray());
byte[] checksum = security().doubleSha512(os.toByteArray());
os.write(checksum, 0, 4);
this.address = "BM-" + Base58.encode(os.toByteArray());
} catch (IOException e) {
@ -82,7 +87,7 @@ public class BitmessageAddress {
}
}
BitmessageAddress(Pubkey publicKey) {
public BitmessageAddress(Pubkey publicKey) {
this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe());
this.pubkey = publicKey;
}
@ -103,18 +108,18 @@ public class BitmessageAddress {
this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20);
// test checksum
byte[] checksum = Security.doubleSha512(bytes, bytes.length - 4);
byte[] checksum = security().doubleSha512(bytes, bytes.length - 4);
byte[] expectedChecksum = bytes(in, 4);
for (int i = 0; i < 4; i++) {
if (expectedChecksum[i] != checksum[i])
throw new IllegalArgumentException("Checksum of address failed");
}
if (version < 4) {
checksum = Security.sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
checksum = security().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
this.tag = null;
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} else {
checksum = Security.doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
checksum = security().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
this.tag = Arrays.copyOfRange(checksum, 32, 64);
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
}
@ -129,7 +134,7 @@ public class BitmessageAddress {
Encode.varInt(version, out);
Encode.varInt(stream, out);
out.write(ripe);
return Arrays.copyOfRange(Security.doubleSha512(out.toByteArray()), 32, 64);
return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64);
} catch (IOException e) {
throw new RuntimeException(e);
}

View File

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

View File

@ -44,10 +44,10 @@ public class GetData implements MessagePayload {
}
@Override
public void write(OutputStream stream) throws IOException {
Encode.varInt(inventory.size(), stream);
public void write(OutputStream out) throws IOException {
Encode.varInt(inventory.size(), out);
for (InventoryVector iv : inventory) {
iv.write(stream);
iv.write(out);
}
}

View File

@ -44,10 +44,10 @@ public class Inv implements MessagePayload {
}
@Override
public void write(OutputStream stream) throws IOException {
Encode.varInt(inventory.size(), stream);
public void write(OutputStream out) throws IOException {
Encode.varInt(inventory.size(), out);
for (InventoryVector iv : inventory) {
iv.write(stream);
iv.write(out);
}
}

View File

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

View File

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

View File

@ -24,12 +24,13 @@ import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.Security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* The 'object' command sends an object that is shared throughout the network.
*/
@ -89,7 +90,9 @@ public class ObjectMessage implements MessagePayload {
}
public InventoryVector getInventoryVector() {
return new InventoryVector(Bytes.truncate(Security.doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32));
return new InventoryVector(
Bytes.truncate(security().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32)
);
}
private boolean isEncrypted() {
@ -113,7 +116,7 @@ public class ObjectMessage implements MessagePayload {
public void sign(PrivateKey key) {
if (payload.isSigned()) {
payload.setSignature(Security.getSignature(getBytesToSign(), key));
payload.setSignature(security().getSignature(getBytesToSign(), key));
}
}
@ -147,12 +150,16 @@ public class ObjectMessage implements MessagePayload {
public boolean isSignatureValid(Pubkey pubkey) throws IOException {
if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first");
return Security.isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey);
return security().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey);
}
@Override
public void write(OutputStream out) throws IOException {
out.write(nonce);
if (nonce != null) {
out.write(nonce);
} else {
out.write(new byte[8]);
}
out.write(getPayloadBytesWithoutNonce());
}

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.UnixTime;
import java.io.*;
import java.util.*;
@ -43,6 +44,7 @@ public class Plaintext implements Streamable {
private Long received;
private Set<Label> labels;
private byte[] initialHash;
private Plaintext(Builder builder) {
id = builder.id;
@ -63,6 +65,7 @@ public class Plaintext implements Streamable {
public static Plaintext read(Type type, InputStream in) throws IOException {
return readWithoutSignature(type, in)
.signature(Decode.varBytes(in))
.received(UnixTime.now())
.build();
}
@ -131,6 +134,15 @@ public class Plaintext implements Streamable {
this.signature = signature;
}
public boolean isUnread() {
for (Label label : labels) {
if (label.getType() == Label.Type.UNREAD) {
return true;
}
}
return false;
}
public void write(OutputStream out, boolean includeSignature) throws IOException {
Encode.varInt(from.getVersion(), out);
Encode.varInt(from.getStream(), out);
@ -249,6 +261,14 @@ public class Plaintext implements Streamable {
}
}
public void setInitialHash(byte[] initialHash) {
this.initialHash = initialHash;
}
public byte[] getInitialHash() {
return initialHash;
}
public enum Encoding {
IGNORE(0), TRIVIAL(1), SIMPLE(2);

View File

@ -18,10 +18,11 @@ package ch.dissem.bitmessage.entity;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
/**
* An object that can be written to an {@link OutputStream}
*/
public interface Streamable {
public interface Streamable extends Serializable {
void write(OutputStream stream) throws IOException;
}

View File

@ -21,11 +21,11 @@ import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.Security;
import java.io.IOException;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* Users who are subscribed to the sending address will see the message appear in their inbox.
@ -78,7 +78,7 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai
}
public void encrypt() throws IOException {
encrypt(Security.createPublicKey(plaintext.getFrom().getPublicDecryptionKey()).getEncoded(false));
encrypt(security().createPublicKey(plaintext.getFrom().getPublicDecryptionKey()));
}
@Override

View File

@ -17,28 +17,16 @@
package ch.dissem.bitmessage.entity.payload;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.*;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.util.Arrays;
import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE;
import static ch.dissem.bitmessage.utils.Singleton.security;
public class CryptoBox implements Streamable {
@ -46,35 +34,38 @@ public class CryptoBox implements Streamable {
private final byte[] initializationVector;
private final int curveType;
private final ECPoint R;
private final byte[] R;
private final byte[] mac;
private byte[] encrypted;
public CryptoBox(Streamable data, byte[] encryptionKey) throws IOException {
this(data, Security.keyToPoint(encryptionKey));
private long addressVersion;
public CryptoBox(Streamable data, byte[] K) throws IOException {
this(Encode.bytes(data), K);
}
public CryptoBox(Streamable data, ECPoint K) throws IOException {
public CryptoBox(byte[] data, byte[] K) throws IOException {
curveType = 0x02CA;
// 1. The destination public key is called K.
// 2. Generate 16 random bytes using a secure random number generator. Call them IV.
initializationVector = Security.randomBytes(16);
initializationVector = security().randomBytes(16);
// 3. Generate a new random EC key pair with private key called r and public key called R.
byte[] r = Security.randomBytes(PRIVATE_KEY_SIZE);
R = Security.createPublicKey(r);
byte[] r = security().randomBytes(PRIVATE_KEY_SIZE);
R = security().createPublicKey(r);
// 4. Do an EC point multiply with public key K and private key r. This gives you public key P.
ECPoint P = K.multiply(Security.keyToBigInt(r)).normalize();
byte[] X = P.getXCoord().getEncoded();
byte[] P = security().multiply(K, r);
byte[] X = Points.getX(P);
// 5. Use the X component of public key P and calculate the SHA512 hash H.
byte[] H = Security.sha512(X);
byte[] H = security().sha512(X);
// 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
byte[] key_e = Arrays.copyOfRange(H, 0, 32);
byte[] key_m = Arrays.copyOfRange(H, 32, 64);
// 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7.
// 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text.
encrypted = crypt(true, Encode.bytes(data), key_e);
encrypted = security().crypt(true, data, key_e, initializationVector);
// 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC.
mac = calculateMac(key_m);
@ -84,7 +75,7 @@ public class CryptoBox implements Streamable {
private CryptoBox(Builder builder) {
initializationVector = builder.initializationVector;
curveType = builder.curveType;
R = Security.createPoint(builder.xComponent, builder.yComponent);
R = security().createPoint(builder.xComponent, builder.yComponent);
encrypted = builder.encrypted;
mac = builder.mac;
}
@ -102,18 +93,17 @@ public class CryptoBox implements Streamable {
}
/**
* @param privateKey a private key, typically should be 32 bytes long
* @param k a private key, typically should be 32 bytes long
* @return an InputStream yielding the decrypted data
* @throws DecryptionFailedException if the payload can't be decrypted using this private key
* @see <a href='https://bitmessage.org/wiki/Encryption#Decryption'>https://bitmessage.org/wiki/Encryption#Decryption</a>
*/
public InputStream decrypt(byte[] privateKey) throws DecryptionFailedException {
public InputStream decrypt(byte[] k) throws DecryptionFailedException {
// 1. The private key used to decrypt is called k.
BigInteger k = Security.keyToBigInt(privateKey);
// 2. Do an EC point multiply with private key k and public key R. This gives you public key P.
ECPoint P = R.multiply(k).normalize();
byte[] P = security().multiply(R, k);
// 3. Use the X component of public key P and calculate the SHA512 hash H.
byte[] H = Security.sha512(P.getXCoord().getEncoded());
byte[] H = security().sha512(Arrays.copyOfRange(P, 1, 33));
// 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
byte[] key_e = Arrays.copyOfRange(H, 0, 32);
byte[] key_m = Arrays.copyOfRange(H, 32, 64);
@ -126,49 +116,28 @@ public class CryptoBox implements Streamable {
// 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key
// and the cipher text as payload. The output is the padded input text.
return new ByteArrayInputStream(crypt(false, encrypted, key_e));
return new ByteArrayInputStream(security().crypt(false, encrypted, key_e, initializationVector));
}
private byte[] calculateMac(byte[] key_m) {
try {
ByteArrayOutputStream macData = new ByteArrayOutputStream();
writeWithoutMAC(macData);
return Security.mac(key_m, macData.toByteArray());
return security().mac(key_m, macData.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private byte[] crypt(boolean encrypt, byte[] data, byte[] key_e) {
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding());
CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector);
cipher.init(encrypt, params);
byte[] buffer = new byte[cipher.getOutputSize(data.length)];
int length = cipher.processBytes(data, 0, data.length, buffer, 0);
try {
length += cipher.doFinal(buffer, length);
} catch (InvalidCipherTextException e) {
throw new IllegalArgumentException(e);
}
if (length < buffer.length) {
return Arrays.copyOfRange(buffer, 0, length);
}
return buffer;
}
private void writeWithoutMAC(OutputStream out) throws IOException {
out.write(initializationVector);
Encode.int16(curveType, out);
writeCoordinateComponent(out, R.getXCoord());
writeCoordinateComponent(out, R.getYCoord());
writeCoordinateComponent(out, Points.getX(R));
writeCoordinateComponent(out, Points.getY(R));
out.write(encrypted);
}
private void writeCoordinateComponent(OutputStream out, ECFieldElement coord) throws IOException {
byte[] x = coord.getEncoded();
private void writeCoordinateComponent(OutputStream out, byte[] x) throws IOException {
int offset = Bytes.numberOfLeadingZeros(x);
int length = x.length - offset;
Encode.int16(length, out);
@ -195,7 +164,7 @@ public class CryptoBox implements Streamable {
}
public Builder curveType(int curveType) {
if (curveType != 0x2CA) LOG.debug("Unexpected curve type " + curveType);
if (curveType != 0x2CA) LOG.trace("Unexpected curve type " + curveType);
this.curveType = curveType;
return this;
}

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.Streamable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
/**
* The payload of an 'object' command. This is shared by the network.

View File

@ -20,8 +20,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import static ch.dissem.bitmessage.utils.Security.ripemd160;
import static ch.dissem.bitmessage.utils.Security.sha512;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* Public keys for signing and encryption, the answer to a 'getpubkey' request.
@ -34,7 +33,7 @@ public abstract class Pubkey extends ObjectPayload {
}
public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) {
return ripemd160(sha512(publicSigningKey, publicEncryptionKey));
return security().ripemd160(security().sha512(publicSigningKey, publicEncryptionKey));
}
public abstract byte[] getSigningKey();
@ -44,7 +43,7 @@ public abstract class Pubkey extends ObjectPayload {
public abstract int getBehaviorBitfield();
public byte[] getRipe() {
return ripemd160(sha512(getSigningKey(), getEncryptionKey()));
return security().ripemd160(security().sha512(getSigningKey(), getEncryptionKey()));
}
public long getNonceTrialsPerByte() {

View File

@ -21,9 +21,10 @@ import ch.dissem.bitmessage.utils.Strings;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Arrays;
public class InventoryVector implements Streamable {
public class InventoryVector implements Streamable, Serializable {
/**
* Hash of the object
*/
@ -37,7 +38,6 @@ public class InventoryVector implements Streamable {
InventoryVector that = (InventoryVector) o;
return Arrays.equals(hash, that.hash);
}
@Override

View File

@ -16,9 +16,10 @@
package ch.dissem.bitmessage.entity.valueobject;
import java.io.Serializable;
import java.util.Objects;
public class Label {
public class Label implements Serializable {
private Object id;
private String label;
private Type type;

View File

@ -18,18 +18,18 @@ package ch.dissem.bitmessage.entity.valueobject;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.Security;
import java.io.*;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* Created by chris on 18.04.15.
* Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying
* {@link Pubkey} object.
*/
public class PrivateKey implements Streamable {
public static final int PRIVATE_KEY_SIZE = 32;
@ -45,15 +45,15 @@ public class PrivateKey implements Streamable {
byte[] pubEK;
byte[] ripe;
do {
privSK = Security.randomBytes(PRIVATE_KEY_SIZE);
privEK = Security.randomBytes(PRIVATE_KEY_SIZE);
pubSK = Security.createPublicKey(privSK).getEncoded(false);
pubEK = Security.createPublicKey(privEK).getEncoded(false);
privSK = security().randomBytes(PRIVATE_KEY_SIZE);
privEK = security().randomBytes(PRIVATE_KEY_SIZE);
pubSK = security().createPublicKey(privSK);
pubEK = security().createPublicKey(privEK);
ripe = Pubkey.getRipe(pubSK, pubEK);
} while (ripe[0] != 0 || (shorter && ripe[1] != 0));
this.privateSigningKey = privSK;
this.privateEncryptionKey = privEK;
this.pubkey = Security.createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey,
this.pubkey = security().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey,
nonceTrialsPerByte, extraBytes, features);
}
@ -66,9 +66,9 @@ public class PrivateKey implements Streamable {
public PrivateKey(long version, long stream, String passphrase, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) {
try {
// 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,
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);

View File

@ -23,7 +23,6 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.NodeException;
import ch.dissem.bitmessage.utils.Security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,6 +31,8 @@ import java.io.InputStream;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* Creates {@link NetworkMessage} objects from {@link InputStream InputStreams}
*/
@ -115,8 +116,8 @@ public class Factory {
BitmessageAddress temp = new BitmessageAddress(address);
PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey,
createPubkey(temp.getVersion(), temp.getStream(),
Security.createPublicKey(privateSigningKey).getEncoded(false),
Security.createPublicKey(privateEncryptionKey).getEncoded(false),
security().createPublicKey(privateSigningKey),
security().createPublicKey(privateEncryptionKey),
nonceTrialsPerByte, extraBytes, behaviourBitfield));
BitmessageAddress result = new BitmessageAddress(privateKey);
if (!result.getAddress().equals(address)) {
@ -126,11 +127,17 @@ public class Factory {
return result;
}
public static BitmessageAddress generatePrivateAddress(boolean shorter, long stream, Pubkey.Feature... features) {
public static BitmessageAddress generatePrivateAddress(boolean shorter,
long stream,
Pubkey.Feature... features) {
return new BitmessageAddress(new PrivateKey(shorter, stream, 1000, 1000, features));
}
static ObjectPayload getObjectPayload(long objectType, long version, long streamNumber, InputStream stream, int length) throws IOException {
static ObjectPayload getObjectPayload(long objectType,
long version,
long streamNumber,
InputStream stream,
int length) throws IOException {
ObjectType type = ObjectType.fromNumber(objectType);
if (type != null) {
switch (type) {
@ -147,7 +154,7 @@ public class Factory {
}
}
// fallback: just store the message - we don't really care what it is
// LOG.info("Unexpected object type: " + objectType);
LOG.trace("Unexpected object type: " + objectType);
return GenericPayload.read(version, stream, streamNumber, length);
}

View File

@ -24,7 +24,6 @@ import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
import ch.dissem.bitmessage.exception.NodeException;
import ch.dissem.bitmessage.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,6 +32,7 @@ import java.io.IOException;
import java.io.InputStream;
import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* Creates protocol v3 network messages from {@link InputStream InputStreams}
@ -44,6 +44,9 @@ class V3MessageFactory {
findMagic(in);
String command = getCommand(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[] payloadBytes = Decode.bytes(in, length);
@ -73,12 +76,18 @@ class V3MessageFactory {
return parseGetData(stream);
case "object":
return readObject(stream, length);
case "custom":
return readCustom(stream, length);
default:
LOG.debug("Unknown command: " + command);
return null;
}
}
private static MessagePayload readCustom(InputStream in, int length) throws IOException {
return CustomMessage.read(in, length);
}
public static ObjectMessage readObject(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter();
byte nonce[] = Decode.bytes(in, 8, counter);
@ -92,7 +101,7 @@ class V3MessageFactory {
try {
ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length);
} catch (IOException e) {
} catch (Exception e) {
LOG.trace("Could not parse object payload - using generic payload instead", e);
payload = new GenericPayload(version, stream, data);
}
@ -174,7 +183,7 @@ class V3MessageFactory {
}
private static boolean testChecksum(byte[] checksum, byte[] payload) {
byte[] payloadChecksum = Security.sha512(payload);
byte[] payloadChecksum = security().sha512(payload);
for (int i = 0; i < checksum.length; i++) {
if (checksum[i] != payloadChecksum[i]) {
return false;
@ -185,10 +194,10 @@ class V3MessageFactory {
private static String getCommand(InputStream stream) throws IOException {
byte[] bytes = new byte[12];
int end = -1;
int end = bytes.length;
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) stream.read();
if (end == -1) {
if (end == bytes.length) {
if (bytes[i] == 0) end = i;
} else {
if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command");

View File

@ -0,0 +1,183 @@
/*
* 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.InternalContext;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import static ch.dissem.bitmessage.utils.Numbers.max;
/**
* Implements everything that isn't directly dependent on either Spongy- or Bouncycastle.
*/
public abstract class AbstractCryptography implements Cryptography, InternalContext.ContextHolder {
public static final Logger LOG = LoggerFactory.getLogger(Cryptography.class);
private static final SecureRandom RANDOM = new SecureRandom();
private static final BigInteger TWO = BigInteger.valueOf(2);
private static final BigInteger TWO_POW_64 = TWO.pow(64);
private static final BigInteger TWO_POW_16 = TWO.pow(16);
private final String provider;
private InternalContext context;
protected AbstractCryptography(String provider) {
this.provider = provider;
}
@Override
public void setContext(InternalContext context) {
this.context = context;
}
public byte[] sha512(byte[]... data) {
return hash("SHA-512", data);
}
public byte[] doubleSha512(byte[]... data) {
MessageDigest mda = md("SHA-512");
for (byte[] d : data) {
mda.update(d);
}
return mda.digest(mda.digest());
}
public byte[] doubleSha512(byte[] data, int length) {
MessageDigest mda = md("SHA-512");
mda.update(data, 0, length);
return mda.digest(mda.digest());
}
public byte[] ripemd160(byte[]... data) {
return hash("RIPEMD160", data);
}
public byte[] doubleSha256(byte[] data, int length) {
MessageDigest mda = md("SHA-256");
mda.update(data, 0, length);
return mda.digest(mda.digest());
}
public byte[] sha1(byte[]... data) {
return hash("SHA-1", data);
}
public byte[] randomBytes(int length) {
byte[] result = new byte[length];
RANDOM.nextBytes(result);
return result;
}
public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte,
long extraBytes, ProofOfWorkEngine.Callback callback) {
nonceTrialsPerByte = max(nonceTrialsPerByte, context.getNetworkNonceTrialsPerByte());
extraBytes = max(extraBytes, context.getNetworkExtraBytes());
byte[] initialHash = getInitialHash(object);
byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes);
context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback);
}
public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
throws IOException {
byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes);
byte[] value = doubleSha512(object.getNonce(), getInitialHash(object));
if (Bytes.lt(target, value, 8)) {
throw new InsufficientProofOfWorkException(target, value);
}
}
@Override
public byte[] getInitialHash(ObjectMessage object) {
return sha512(object.getPayloadBytesWithoutNonce());
}
@Override
public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
if (nonceTrialsPerByte == 0) nonceTrialsPerByte = context.getNetworkNonceTrialsPerByte();
if (extraBytes == 0) extraBytes = context.getNetworkExtraBytes();
BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now());
BigInteger numerator = TWO_POW_64;
BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes);
BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte)
.multiply(
powLength.add(
powLength.multiply(TTL).divide(TWO_POW_16)
)
);
return Bytes.expand(numerator.divide(denominator).toByteArray(), 8);
}
private byte[] hash(String algorithm, byte[]... data) {
MessageDigest mda = md(algorithm);
for (byte[] d : data) {
mda.update(d);
}
return mda.digest();
}
private MessageDigest md(String algorithm) {
try {
return MessageDigest.getInstance(algorithm, provider);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public byte[] mac(byte[] key_m, byte[] data) {
try {
Mac mac = Mac.getInstance("HmacSHA256", provider);
mac.init(new SecretKeySpec(key_m, "HmacSHA256"));
return mac.doFinal(data);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey,
long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) {
return Factory.createPubkey(version, stream,
createPublicKey(privateSigningKey),
createPublicKey(privateEncryptionKey),
nonceTrialsPerByte, extraBytes, features);
}
public BigInteger keyToBigInt(byte[] privateKey) {
return new BigInteger(1, privateKey);
}
public long randomNonce() {
return RANDOM.nextLong();
}
}

View File

@ -0,0 +1,210 @@
/*
* 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.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.SecureRandom;
/**
* Provides some methods to help with hashing and encryption. All randoms are created using {@link SecureRandom},
* which should be secure enough.
*/
public interface Cryptography {
/**
* 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
* success on the same thread.
*
* @param data to get hashed
* @return SHA-512 hash of data
*/
byte[] sha512(byte[]... data);
/**
* A helper method to calculate doubleSHA-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
* success on the same thread.
*
* @param data to get hashed
* @return SHA-512 hash of data
*/
byte[] doubleSha512(byte[]... data);
/**
* A helper method to calculate double SHA-512 hashes. This method allows to only use a part of the available bytes
* to use for the hash calculation.
* <p>
* 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 short order on the same thread.
* </p>
*
* @param data to get hashed
* @param length number of bytes to be taken into account
* @return SHA-512 hash of data
*/
byte[] doubleSha512(byte[] data, int length);
/**
* A helper method to calculate RIPEMD-160 hashes. Supplying multiple byte arrays has the same result as a
* concatenation of all arrays, but might perform better.
* <p>
* 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 short
* order on the same thread.
* </p>
*
* @param data to get hashed
* @return RIPEMD-160 hash of data
*/
byte[] ripemd160(byte[]... data);
/**
* A helper method to calculate double SHA-256 hashes. This method allows to only use a part of the available bytes
* to use for the hash calculation.
* <p>
* 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 short
* order on the same thread.
* </p>
*
* @param data to get hashed
* @param length number of bytes to be taken into account
* @return SHA-256 hash of data
*/
byte[] doubleSha256(byte[] data, int length);
/**
* A helper method to calculate SHA-1 hashes. Supplying multiple byte arrays has the same result as a
* concatenation of all arrays, but might perform better.
* <p>
* 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 short
* order on the same thread.
* </p>
*
* @param data to get hashed
* @return SHA hash of data
*/
byte[] sha1(byte[]... data);
/**
* @param length number of bytes to return
* @return an array of the given size containing random bytes
*/
byte[] randomBytes(int length);
/**
* Calculates the proof of work. This might take a long time, depending on the hardware, message size and time to
* live.
*
* @param object to do the proof of work for
* @param nonceTrialsPerByte difficulty
* @param extraBytes bytes to add to the object size (makes it more difficult to send small messages)
* @param callback to handle nonce once it's calculated
*/
void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte,
long extraBytes, ProofOfWorkEngine.Callback callback);
/**
* @param object to be checked
* @param nonceTrialsPerByte difficulty
* @param extraBytes bytes to add to the object size
* @throws InsufficientProofOfWorkException if proof of work doesn't check out (makes it more difficult to send small messages)
*/
void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
throws IOException;
byte[] getInitialHash(ObjectMessage object);
byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
/**
* Calculates the MAC for a message (data)
*
* @param key_m the symmetric key used
* @param data the message data to calculate the MAC for
* @return the MAC
*/
byte[] mac(byte[] key_m, byte[] data);
/**
* @param encrypt if true, encrypts data, otherwise tries to decrypt it.
* @param data
* @param key_e
* @return
*/
byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector);
/**
* Create a new public key fom given private keys.
*
* @param version of the public key / address
* @param stream of the address
* @param privateSigningKey private key used for signing
* @param privateEncryptionKey private key used for encryption
* @param nonceTrialsPerByte proof of work difficulty
* @param extraBytes bytes to add for the proof of work (make it harder for small messages)
* @param features of the address
* @return a public key object
*/
Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey,
long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features);
/**
* @param privateKey private key as byte array
* @return a public key corresponding to the given private key
*/
byte[] createPublicKey(byte[] privateKey);
/**
* @param privateKey private key as byte array
* @return a big integer representation (unsigned) of the given bytes
*/
BigInteger keyToBigInt(byte[] privateKey);
/**
* @param data to check
* @param signature the signature of the message
* @param pubkey the sender's public key
* @return true if the signature is valid, false otherwise
*/
boolean isSignatureValid(byte[] data, byte[] signature, Pubkey pubkey);
/**
* Calculate the signature of data, using the given private key.
*
* @param data to be signed
* @param privateKey to be used for signing
* @return the signature
*/
byte[] getSignature(byte[] data, ch.dissem.bitmessage.entity.valueobject.PrivateKey privateKey);
/**
* @return a random number of type long
*/
long randomNonce();
byte[] multiply(byte[] k, byte[] r);
byte[] createPoint(byte[] x, byte[] y);
}

View File

@ -16,25 +16,12 @@
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.utils.Property;
import java.io.IOException;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.MessagePayload;
/**
* Handles incoming messages
* @author Christian Basler
*/
public interface NetworkHandler {
void start(MessageListener listener);
void stop();
void offer(InventoryVector iv);
Property getNetworkStatus();
interface MessageListener {
void receive(ObjectMessage object) throws IOException;
}
public interface CustomCommandHandler {
MessagePayload handle(CustomMessage request);
}

View File

@ -26,15 +26,32 @@ import java.util.List;
* The Inventory stores and retrieves objects, cleans up outdated objects and can tell which objects are still missing.
*/
public interface Inventory {
/**
* Returns the IVs of all valid objects we have for the given streams
*/
List<InventoryVector> getInventory(long... streams);
/**
* Returns the IVs of all objects in the offer that we don't have already. Implementations are allowed to
* ignore the streams parameter, but it must be set when calling this method.
*/
List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams);
ObjectMessage getObject(InventoryVector vector);
/**
* This method is mainly used to search for public keys to newly added addresses or broadcasts from new
* subscriptions.
*/
List<ObjectMessage> getObjects(long stream, long version, ObjectType... types);
void storeObject(ObjectMessage object);
boolean contains(ObjectMessage object);
/**
* Deletes all objects that expired 5 minutes ago or earlier
* (so we don't accidentally request objects we just deleted)
*/
void cleanup();
}

View File

@ -14,10 +14,9 @@
* limitations under the License.
*/
package ch.dissem.bitmessage.repository;
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
import ch.dissem.bitmessage.ports.NodeRegistry;
import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,10 +34,10 @@ import static java.util.Collections.newSetFromMap;
public class MemoryNodeRegistry implements NodeRegistry {
private static final Logger LOG = LoggerFactory.getLogger(MemoryNodeRegistry.class);
private final Map<Long, Set<NetworkAddress>> stableNodes = new HashMap<>();
private final Map<Long, Set<NetworkAddress>> stableNodes = new ConcurrentHashMap<>();
private final Map<Long, Set<NetworkAddress>> knownNodes = new ConcurrentHashMap<>();
public MemoryNodeRegistry() {
private void loadStableNodes() {
try (InputStream in = getClass().getClassLoader().getResourceAsStream("nodes.txt")) {
Scanner scanner = new Scanner(in);
long stream = 0;
@ -56,14 +55,21 @@ public class MemoryNodeRegistry implements NodeRegistry {
stableNodes.put(stream, streamSet);
} else if (streamSet != null) {
int portIndex = line.lastIndexOf(':');
InetAddress inetAddress = InetAddress.getByName(line.substring(0, portIndex));
InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex));
int port = Integer.valueOf(line.substring(portIndex + 1));
streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build());
for (InetAddress inetAddress : inetAddresses) {
streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build());
}
}
} catch (IOException e) {
LOG.warn(e.getMessage(), e);
}
}
if (LOG.isDebugEnabled()) {
for (Map.Entry<Long, Set<NetworkAddress>> e : stableNodes.entrySet()) {
LOG.debug("Stream " + e.getKey() + ": loaded " + e.getValue().size() + " bootstrap nodes.");
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
@ -82,9 +88,16 @@ public class MemoryNodeRegistry implements NodeRegistry {
known.remove(node);
}
}
} else if (stableNodes.containsKey(stream)) {
// To reduce load on stable nodes, only return one
result.add(selectRandom(stableNodes.get(stream)));
} else {
Set<NetworkAddress> nodes = stableNodes.get(stream);
if (nodes == null || nodes.isEmpty()) {
loadStableNodes();
nodes = stableNodes.get(stream);
}
if (nodes != null && !nodes.isEmpty()) {
// To reduce load on stable nodes, only return one
result.add(selectRandom(nodes));
}
}
}
return selectRandom(limit, result);

View File

@ -28,12 +28,18 @@ public interface MessageRepository {
List<Label> getLabels(Label.Type... types);
int countUnread(Label label);
Plaintext getMessage(byte[] initialHash);
List<Plaintext> findMessages(Label label);
List<Plaintext> findMessages(Status status);
List<Plaintext> findMessages(Status status, BitmessageAddress recipient);
List<Plaintext> findMessages(BitmessageAddress sender);
void save(Plaintext message);
void remove(Plaintext message);

View File

@ -24,6 +24,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import static ch.dissem.bitmessage.utils.Bytes.inc;
@ -31,46 +32,53 @@ import static ch.dissem.bitmessage.utils.Bytes.inc;
* A POW engine using all available CPU cores.
*/
public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
private static Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class);
private static final Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class);
private static final Semaphore semaphore = new Semaphore(1, true);
/**
* This method will block until all pending nonce calculations are done, but not wait for its own calculation
* to finish.
* (This implementation becomes very inefficient if multiple nonce are calculated at the same time.)
*
* @param initialHash the SHA-512 hash of the object to send, sans nonce
* @param target the target, representing an unsigned long
* @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make
*/
@Override
public byte[] calculateNonce(byte[] initialHash, byte[] target) {
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
callback = new CallbackWrapper(callback);
int cores = Runtime.getRuntime().availableProcessors();
if (cores > 255) cores = 255;
LOG.info("Doing POW using " + cores + " cores");
long time = System.currentTimeMillis();
List<Worker> workers = new ArrayList<>(cores);
for (int i = 0; i < cores; i++) {
Worker w = new Worker(workers, (byte) cores, i, initialHash, target);
Worker w = new Worker(workers, (byte) cores, i, initialHash, target, callback);
workers.add(w);
}
for (Worker w : workers) {
// Doing this in the previous loop might cause a ConcurrentModificationException in the worker
// if a worker finds a nonce while new ones are still being added.
w.start();
}
for (Worker w : workers) {
try {
w.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (w.isSuccessful()) {
LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - time) / 1000) + " seconds");
return w.getNonce();
}
}
throw new RuntimeException("All workers ended without yielding a nonce - something is seriously broken!");
}
private static class Worker extends Thread {
private final Callback callback;
private final byte numberOfCores;
private final List<Worker> workers;
private final byte[] initialHash;
private final byte[] target;
private final MessageDigest mda;
private final byte[] nonce = new byte[8];
private boolean successful = false;
public Worker(List<Worker> workers, byte numberOfCores, int core, byte[] initialHash, byte[] target) {
public Worker(List<Worker> workers, byte numberOfCores, int core, byte[] initialHash, byte[] target,
Callback callback) {
this.callback = callback;
this.numberOfCores = numberOfCores;
this.workers = workers;
this.initialHash = initialHash;
@ -84,14 +92,6 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
}
}
public boolean isSuccessful() {
return successful;
}
public byte[] getNonce() {
return nonce;
}
@Override
public void run() {
do {
@ -99,13 +99,43 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
mda.update(nonce);
mda.update(initialHash);
if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) {
successful = true;
for (Worker w : workers) {
w.interrupt();
synchronized (callback) {
if (!Thread.interrupted()) {
for (Worker w : workers) {
w.interrupt();
}
// Clear interrupted flag for callback
Thread.interrupted();
callback.onNonceCalculated(initialHash, nonce);
}
}
return;
}
} while (!Thread.interrupted());
}
}
public static class CallbackWrapper implements Callback {
private final Callback callback;
private final long startTime;
private boolean waiting = true;
public CallbackWrapper(Callback callback) {
this.startTime = System.currentTimeMillis();
this.callback = callback;
}
@Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
// Prevents the callback from being called twice if two nonces are found simultaneously
synchronized (this) {
if (waiting) {
semaphore.release();
LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds");
waiting = false;
callback.onNonceCalculated(initialHash, nonce);
}
}
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.ObjectMessage;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.utils.Property;
import java.io.IOException;
import java.net.InetAddress;
import java.util.concurrent.Future;
/**
* Handles incoming messages
*/
public interface NetworkHandler {
/**
* Connects to the trusted host, fetches and offers new messages and disconnects afterwards.
* <p>
* An implementation should disconnect if either the timeout is reached or the returned thread is interrupted.
* </p>
*/
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.
*/
void start(MessageListener listener);
/**
* Stop the full network node.
*/
void stop();
/**
* Offer new objects to up to 8 random nodes.
*/
void offer(InventoryVector iv);
Property getNetworkStatus();
boolean isRunning();
interface MessageListener {
void receive(ObjectMessage object) throws IOException;
}
}

View File

@ -24,9 +24,17 @@ public interface ProofOfWorkEngine {
* Returns a nonce, such that the first 8 bytes from sha512(sha512(nonce||initialHash)) represent a unsigned long
* smaller than target.
*
* @param initialHash the SHA-512 hash of the object to send, sans nonce
* @param target the target, representing an unsigned long
* @return 8 bytes nonce
* @param initialHash the SHA-512 hash of the object to send, sans nonce
* @param target the target, representing an unsigned long
* @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make
* sure this is only called once.
*/
byte[] calculateNonce(byte[] initialHash, byte[] target);
void calculateNonce(byte[] initialHash, byte[] target, Callback callback);
interface Callback {
/**
* @param nonce 8 bytes 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

@ -23,11 +23,15 @@ import java.security.MessageDigest;
import static ch.dissem.bitmessage.utils.Bytes.inc;
/**
* Created by chris on 14.04.15.
* 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 {
@Override
public byte[] calculateNonce(byte[] initialHash, byte[] target) {
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
byte[] nonce = new byte[8];
MessageDigest mda;
try {
@ -40,6 +44,6 @@ public class SimplePOWEngine implements ProofOfWorkEngine {
mda.update(nonce);
mda.update(initialHash);
} while (Bytes.lt(target, mda.digest(mda.digest()), 8));
return nonce;
callback.onNonceCalculated(initialHash, nonce);
}
}

View File

@ -16,12 +16,6 @@
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.Streamable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* A helper class for working with byte arrays interpreted as unsigned big endian integers.
* This is one part due to the fact that Java doesn't support unsigned numbers, and another
@ -91,6 +85,8 @@ public class Bytes {
if (a < 0) return b < 0 && a < b;
if (b < 0) return a >= 0 || a < b;
return a < b;
// This would be easier to understand, but is (slightly) slower:
// return (a & 0xff) < (b & 0xff);
}
/**

View File

@ -0,0 +1,48 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
/**
* Waits for a value within a callback method to be set.
*/
public class CallbackWaiter<T> {
private final long startTime = System.currentTimeMillis();
private volatile boolean isSet;
private T value;
private long time;
public void setValue(T value) {
synchronized (this) {
this.time = System.currentTimeMillis() - startTime;
this.value = value;
this.isSet = true;
}
}
public T waitForValue() throws InterruptedException {
while (!isSet) {
Thread.sleep(100);
}
synchronized (this) {
return value;
}
}
public long getTime() {
return time;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import java.util.Arrays;
/**
* Created by chris on 20.07.15.
*/
public class Points {
public static byte[] getX(byte[] P) {
return Arrays.copyOfRange(P, 1, ((P.length - 1) / 2) + 1);
}
public static byte[] getY(byte[] P) {
return Arrays.copyOfRange(P, ((P.length - 1) / 2) + 1, P.length);
}
}

View File

@ -16,8 +16,16 @@
package ch.dissem.bitmessage.utils;
import java.util.Arrays;
import java.util.Objects;
/**
* Created by chris on 14.06.15.
* 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
* notation, but you might only want to rely on the 'human readable' part.
* <p>
* If you need a real JSON representation, please add a method <code>toJson()</code>.
* </p>
*/
public class Property {
private String name;
@ -30,6 +38,36 @@ public class Property {
this.properties = properties;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
/**
* 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) {
if (Objects.equals(name[0], p.name)) {
if (name.length == 1)
return p;
else
return p.getProperty(Arrays.copyOfRange(name, 1, name.length));
}
}
return null;
}
public Property[] getProperties() {
return properties;
}
@Override
public String toString() {
return toString("");

View File

@ -0,0 +1,38 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.ports.Cryptography;
/**
* Created by chris on 20.07.15.
*/
public class Singleton {
private static Cryptography cryptography;
public static void initialize(Cryptography cryptography) {
synchronized (Singleton.class) {
if (Singleton.cryptography == null) {
Singleton.cryptography = cryptography;
}
}
}
public static Cryptography security() {
return cryptography;
}
}

View File

@ -0,0 +1,8 @@
[stream 1]
dissem.ch:8444
bootstrap8080.bitmessage.org:8080
bootstrap8444.bitmessage.org:8444
[stream 2]
# none yet

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.V4Broadcast;
import ch.dissem.bitmessage.entity.payload.V5Broadcast;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
@ -29,7 +30,7 @@ import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class DecryptionTest {
public class DecryptionTest extends TestBase {
@Test
public void ensureV4BroadcastIsDecryptedCorrectly() throws IOException, DecryptionFailedException {
BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ");

View File

@ -24,20 +24,20 @@ import ch.dissem.bitmessage.entity.payload.GenericPayload;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.Security;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
import java.io.IOException;
import static ch.dissem.bitmessage.utils.Singleton.security;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class EncryptionTest {
public class EncryptionTest extends TestBase {
@Test
public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException {
GenericPayload before = new GenericPayload(0, 1, Security.randomBytes(100));
GenericPayload before = new GenericPayload(0, 1, security().randomBytes(100));
PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000);
CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey());

View File

@ -25,6 +25,7 @@ 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.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
@ -33,7 +34,7 @@ import java.util.Date;
import static org.junit.Assert.*;
public class SignatureTest {
public class SignatureTest extends TestBase {
@Test
public void ensureValidationWorks() throws IOException {
ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload");

View File

@ -27,6 +27,7 @@ import java.io.IOException;
import java.util.Arrays;
import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK;
import static ch.dissem.bitmessage.utils.Singleton.security;
import static org.junit.Assert.*;
public class BitmessageAddressTest {
@ -102,7 +103,7 @@ public class BitmessageAddressTest {
System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
Security.createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000)));
security().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals(address_string, address.getAddress());
}
@ -119,7 +120,7 @@ public class BitmessageAddressTest {
if (bytes.length != 37)
throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long");
byte[] hash = Security.doubleSha256(bytes, 33);
byte[] hash = security().doubleSha256(bytes, 33);
for (int i = 0; i < 4; i++) {
if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat);
}
@ -132,7 +133,7 @@ public class BitmessageAddressTest {
byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU");
byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
Security.createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress());
}

View File

@ -17,21 +17,23 @@
package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static ch.dissem.bitmessage.utils.Singleton.security;
import static org.junit.Assert.*;
public class SerializationTest {
public class SerializationTest extends TestBase {
@Test
public void ensureGetPubkeyIsDeserializedAndSerializedCorrectly() throws IOException {
doTest("V2GetPubkey.payload", 2, GetPubkey.class);
@ -75,7 +77,7 @@ public class SerializationTest {
}
@Test
public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws IOException, DecryptionFailedException {
public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws Exception {
Plaintext p1 = new Plaintext.Builder(MSG)
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
@ -87,16 +89,57 @@ public class SerializationTest {
p1.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
Plaintext p2 = Plaintext.read(MSG, in);
// Received is automatically set on deserialization, so we'll need to set it to 0
Field received = Plaintext.class.getDeclaredField("received");
received.setAccessible(true);
received.set(p2, 0L);
assertEquals(p1, p2);
}
@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()));
Inv invAfter = (Inv) after.getPayload();
assertEquals(ivs, invAfter.getInventory());
}
private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException {
byte[] data = TestUtils.getBytes(resourceName);
InputStream in = new ByteArrayInputStream(data);
ObjectMessage object = Factory.getObjectMessage(version, in, data.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertNotNull(object);
object.write(out);
assertArrayEquals(data, out.toByteArray());
assertEquals(expectedPayloadType.getCanonicalName(), object.getPayload().getClass().getCanonicalName());
}
@Test
public void ensureSystemSerializationWorks() throws Exception {
Plaintext plaintext = new Plaintext.Builder(MSG)
.from(TestUtils.loadContact())
.to(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.labels(Collections.singletonList(new Label("Test", Label.Type.INBOX, 0)))
.message("Test", "Test Test.\nTest")
.build();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(plaintext);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
assertEquals(plaintext, ois.readObject());
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.utils.Bytes;
import ch.dissem.bitmessage.utils.CallbackWaiter;
import ch.dissem.bitmessage.utils.TestBase;
import org.junit.Test;
import static ch.dissem.bitmessage.utils.Singleton.security;
import static org.junit.Assert.assertTrue;
public class ProofOfWorkEngineTest extends TestBase {
@Test(timeout = 90_000)
public void testSimplePOWEngine() throws InterruptedException {
testPOW(new SimplePOWEngine());
}
@Test(timeout = 90_000)
public void testThreadedPOWEngine() throws InterruptedException {
testPOW(new MultiThreadedPOWEngine());
}
private void testPOW(ProofOfWorkEngine engine) throws InterruptedException {
byte[] initialHash = security().sha512(new byte[]{1, 3, 6, 4});
byte[] target = {0, 0, 0, -1, -1, -1, -1, -1};
final CallbackWaiter<byte[]> waiter1 = new CallbackWaiter<>();
engine.calculateNonce(initialHash, target,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter1.setValue(nonce);
}
});
byte[] nonce = waiter1.waitForValue();
System.out.println("Calculating nonce took " + waiter1.getTime() + "ms");
assertTrue(Bytes.lt(security().doubleSha512(nonce, initialHash), target, 8));
// Let's add a second (shorter) run to find possible multi threading issues
byte[] initialHash2 = security().sha512(new byte[]{1, 3, 6, 5});
byte[] target2 = {0, 0, -1, -1, -1, -1, -1, -1};
final CallbackWaiter<byte[]> waiter2 = new CallbackWaiter<>();
engine.calculateNonce(initialHash2, target2,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter2.setValue(nonce);
}
});
byte[] nonce2 = waiter2.waitForValue();
System.out.println("Calculating nonce took " + waiter2.getTime() + "ms");
assertTrue(Bytes.lt(security().doubleSha512(nonce2, initialHash2), target2, 8));
assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime());
}
}

View File

@ -16,6 +16,7 @@
package ch.dissem.bitmessage.utils;
import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
@ -25,16 +26,13 @@ import java.util.Random;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/**
* Created by chris on 10.04.15.
*/
public class BytesTest {
public static final Random rnd = new Random();
@Test
public void ensureExpandsCorrectly() {
byte[] source = {1};
byte[] expected = {0,1};
byte[] expected = {0, 1};
assertArrayEquals(expected, Bytes.expand(source, 2));
}
@ -56,6 +54,24 @@ public class BytesTest {
}
}
/**
* This test is used to compare different implementations of the single byte lt comparison. It an safely be ignored.
*/
@Test
@Ignore
public void testLowerThanSingleByte() {
byte[] a = new byte[1];
byte[] b = new byte[1];
for (int i = 0; i < 255; i++) {
for (int j = 0; j < 255; j++) {
System.out.println("a = " + i + "\tb = " + j);
a[0] = (byte) i;
b[0] = (byte) j;
assertEquals(i < j, Bytes.lt(a, b));
}
}
}
@Test
public void testLowerThan() {
for (int i = 0; i < 1000; i++) {

View File

@ -22,9 +22,6 @@ import java.io.*;
import static org.junit.Assert.assertEquals;
/**
* Created by chris on 20.03.15.
*/
public class DecodeTest {
@Test
public void ensureDecodingWorks() throws Exception {

View File

@ -23,9 +23,6 @@ import java.io.IOException;
import static org.junit.Assert.assertEquals;
/**
* Created by chris on 13.03.15.
*/
public class EncodeTest {
@Test
public void testUint8() throws IOException {

View File

@ -0,0 +1,28 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography;
/**
* Created by chris on 20.07.15.
*/
public class TestBase {
static {
Singleton.initialize(new BouncyCryptography());
}
}

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