diff --git a/.travis.yml b/.travis.yml index 9bcf999..aacb38f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ language: java +sudo: false # faster builds jdk: - oraclejdk8 + +before_install: + - pip install --user codecov + +after_success: + - codecov diff --git a/Bitmessage.uml b/Bitmessage.uml index 997ab34..598f220 100644 --- a/Bitmessage.uml +++ b/Bitmessage.uml @@ -3,325 +3,1306 @@ JAVA - ch.dissem.bitmessage.entity.Encrypted - ch.dissem.bitmessage.ports.Inventory - ch.dissem.bitmessage.entity.payload.V4Pubkey - ch.dissem.bitmessage.entity.Addr - ch.dissem.bitmessage.entity.payload.Broadcast - ch.dissem.bitmessage.factory.Factory - ch.dissem.bitmessage.entity.valueobject.NetworkAddress - ch.dissem.bitmessage.entity.payload.V2Pubkey - ch.dissem.bitmessage.ports.AddressRepository - ch.dissem.bitmessage.entity.payload.V3Pubkey - ch.dissem.bitmessage.entity.payload.ObjectPayload - ch.dissem.bitmessage.entity.MessagePayload - ch.dissem.bitmessage.entity.NetworkMessage - ch.dissem.bitmessage.entity.Version - ch.dissem.bitmessage.BitmessageContext - ch.dissem.bitmessage.ports.ProofOfWorkEngine - ch.dissem.bitmessage.entity.BitmessageAddress - ch.dissem.bitmessage.entity.payload.UnencryptedMessage - ch.dissem.bitmessage.factory.V3MessageFactory - ch.dissem.bitmessage.entity.payload.CryptoBox - ch.dissem.bitmessage.entity.valueobject.InventoryVector - ch.dissem.bitmessage.entity.payload.V5Broadcast - ch.dissem.bitmessage.entity.valueobject.PrivateKey - ch.dissem.bitmessage.ports.MultiThreadedPOWEngine - ch.dissem.bitmessage.entity.Inv - ch.dissem.bitmessage.entity.payload.Pubkey - ch.dissem.bitmessage.entity.payload.GetPubkey - ch.dissem.bitmessage.entity.Streamable - ch.dissem.bitmessage.entity.payload.ObjectType - ch.dissem.bitmessage.entity.ObjectMessage - ch.dissem.bitmessage.entity.payload.GenericPayload - ch.dissem.bitmessage.ports.NetworkHandler - ch.dissem.bitmessage.entity.VerAck - ch.dissem.bitmessage.entity.GetData - ch.dissem.bitmessage.entity.payload.Msg - ch.dissem.bitmessage.ports.NodeRegistry - ch.dissem.bitmessage.entity.payload.V4Broadcast + ch.dissem.bitmessage.entity.valueobject.Label.Type + ch.dissem.bitmessage.networking.Connection.WriterRunnable + ch.dissem.bitmessage.entity.valueobject.NetworkAddress + ch.dissem.bitmessage.factory.V3MessageFactory + ch.dissem.bitmessage.ProofOfWorkService + ch.dissem.bitmessage.cryptography.bc.BouncyCryptography + ch.dissem.bitmessage.entity.ObjectMessage + ch.dissem.bitmessage.repository.JdbcHelper + ch.dissem.bitmessage.exception.InsufficientProofOfWorkException + ch.dissem.bitmessage.utils.AccessCounter + ch.dissem.bitmessage.MessageCallback + ch.dissem.bitmessage.networking.Connection.ReaderRunnable + ch.dissem.bitmessage.entity.payload.V2Pubkey + ch.dissem.bitmessage.extensions.CryptoCustomMessage.SignatureCheckingInputStream + ch.dissem.bitmessage.entity.Plaintext + ch.dissem.bitmessage.InternalContext + ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest + ch.dissem.bitmessage.ports.NetworkHandler.MessageListener + ch.dissem.bitmessage.entity.payload.Msg + ch.dissem.bitmessage.networking.Connection.Mode + ch.dissem.bitmessage.utils.Singleton + ch.dissem.bitmessage.ports.Cryptography + ch.dissem.bitmessage.ports.ProofOfWorkRepository + ch.dissem.bitmessage.repository.JdbcMessageRepository + ch.dissem.bitmessage.repository.JdbcInventory + ch.dissem.bitmessage.exception.NodeException + ch.dissem.bitmessage.entity.payload.GetPubkey + ch.dissem.bitmessage.entity.GetData + ch.dissem.bitmessage.entity.Addr + ch.dissem.bitmessage.InternalContext.ContextHolder + ch.dissem.bitmessage.entity.CustomMessage + ch.dissem.bitmessage.DefaultMessageListener + ch.dissem.bitmessage.ports.MultiThreadedPOWEngine.Worker + ch.dissem.bitmessage.ports.MultiThreadedPOWEngine.CallbackWrapper + ch.dissem.bitmessage.ports.CustomCommandHandler + ch.dissem.bitmessage.utils.Property + ch.dissem.bitmessage.repository.JdbcAddressRepository + ch.dissem.bitmessage.BitmessageContext + ch.dissem.bitmessage.entity.VerAck + ch.dissem.bitmessage.repository.JdbcConfig + ch.dissem.bitmessage.ports.MemoryNodeRegistry + ch.dissem.bitmessage.entity.valueobject.InventoryVector + ch.dissem.bitmessage.entity.payload.V4Pubkey + ch.dissem.bitmessage.entity.payload.V5Broadcast + ch.dissem.bitmessage.entity.Inv + ch.dissem.bitmessage.ports.AddressRepository + ch.dissem.bitmessage.entity.payload.Pubkey + ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request + ch.dissem.bitmessage.ports.MessageRepository + ch.dissem.bitmessage.wif.WifExporter + ch.dissem.bitmessage.entity.valueobject.PrivateKey + ch.dissem.bitmessage.ports.MultiThreadedPOWEngine + ch.dissem.bitmessage.extensions.CryptoCustomMessage.Reader + ch.dissem.bitmessage.entity.payload.V3Pubkey + ch.dissem.bitmessage.entity.payload.Pubkey.Feature + ch.dissem.bitmessage.ports.NetworkHandler + ch.dissem.bitmessage.ports.AbstractCryptography + ch.dissem.bitmessage.cryptography.sc.SpongyCryptography + ch.dissem.bitmessage.ports.SimplePOWEngine + ch.dissem.bitmessage.ports.NodeRegistry + ch.dissem.bitmessage.wif.WifImporter + ch.dissem.bitmessage.entity.MessagePayload.Command + ch.dissem.bitmessage.entity.NetworkMessage + ch.dissem.bitmessage.entity.Plaintext.Encoding + ch.dissem.bitmessage.entity.Plaintext.Type + ch.dissem.bitmessage.entity.payload.CryptoBox + ch.dissem.bitmessage.factory.Factory + ch.dissem.bitmessage.networking.DefaultNetworkHandler + ch.dissem.bitmessage.repository.JdbcProofOfWorkRepository + ch.dissem.bitmessage.entity.BitmessageAddress + ch.dissem.bitmessage.networking.Connection.State + ch.dissem.bitmessage.entity.payload.V4Broadcast + ch.dissem.bitmessage.entity.Encrypted + ch.dissem.bitmessage.networking.Connection + ch.dissem.bitmessage.ports.ProofOfWorkEngine + ch.dissem.bitmessage.entity.MessagePayload + ch.dissem.bitmessage.entity.Streamable + ch.dissem.bitmessage.BitmessageContext.Listener + ch.dissem.bitmessage.ports.Inventory + ch.dissem.bitmessage.ports.ProofOfWorkEngine.Callback + ch.dissem.bitmessage.entity.payload.GenericPayload + ch.dissem.bitmessage.entity.Version + ch.dissem.bitmessage.entity.payload.ObjectPayload + ch.dissem.bitmessage.entity.payload.Broadcast + ch.dissem.bitmessage.extensions.CryptoCustomMessage + ch.dissem.bitmessage.exception.DecryptionFailedException + ch.dissem.bitmessage.entity.Plaintext.Status + ch.dissem.bitmessage.entity.PlaintextHolder + ch.dissem.bitmessage.entity.valueobject.Label + ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item - - - + + + + + + + + + - - - + + + + + - - - - - + + + + + + + - - - + + + + + - - - + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - + + + + - - - - + + + + - - - - - - - + + + + + - - - + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + - - - - - + + + + + - - - + + + + + - - - + + + + + - - - + + + + + - - - - - - - + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + - - - + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - Fields - Methods - Properties + Inner Classes - All + Production protected diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..34c84ba --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index eb5cdfc..6c44558 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,38 @@ -Jabit [![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) +Jabit ===== +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core) +[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.jabit/jabit-core/badge.svg)](http://www.javadoc.io/doc/ch.dissem.jabit/jabit-core) +[![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE) +[![Visit our IRC channel](https://img.shields.io/badge/irc-%23jabit-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#jabit) A Java implementation for the Bitmessage protocol. To build, use command `gradle build` or `./gradlew build`. -Please note that development is still heavily in progress, and I will break the database a lot until it's ready for prime time. +Please note that it still has its limitations, but the API should now be stable. Jabit uses Semantic Versioning, meaning +as long as the major version doesn't change, nothing should break if you update. + +#### Master +[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) +[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/master.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144281) +[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=master)](https://codecov.io/github/Dissem/Jabit?branch=master) + +#### Develop +[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=develop)](https://travis-ci.org/Dissem/Jabit?branch=develop) +[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/develop.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144279) +[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=develop)](https://codecov.io/github/Dissem/Jabit?branch=develop) Security -------- -There are most probably some security issues, me programming this thing all by myself. Jabit doesn't do anything against timing attacks yet, for example. Please feel free to use the library, report bugs and maybe even help out. I hope the code is easy to understand and work with. +There are most probably some security issues, me programming this thing all by myself. Jabit doesn't do anything against +timing attacks yet, for example. Please feel free to use the library, report bugs and maybe even help out. I hope the +code is easy to understand and work with. Project Status -------------- Basically, everything needed for a working Bitmessage client is there: * Creating new identities (private addresses) -* Adding contracts and subscriptions +* Adding contacts and subscriptions * Receiving broadcasts * Receiving messages * Sending messages and broadcasts @@ -29,18 +46,21 @@ Setup Add Jabit as Gradle dependency: ```Gradle -compile 'ch.dissem.jabit:jabit-domain:0.2.0' +compile 'ch.dissem.jabit:jabit-core:1.0.0' ``` Unless you want to implement your own, also add the following: ```Gradle -compile 'ch.dissem.jabit:jabit-networking:0.2.0' -compile 'ch.dissem.jabit:jabit-repositories:0.2.0' +compile 'ch.dissem.jabit:jabit-networking:1.0.0' +compile 'ch.dissem.jabit:jabit-repositories:1.0.0' +compile 'ch.dissem.jabit:jabit-cryptography-bouncy:1.0.0' ``` And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add: ```Gradle -compile 'ch.dissem.jabit:jabit-wif:0.2.0' +compile 'ch.dissem.jabit:jabit-wif:1.0.0' ``` +For Android clients use `jabit-cryptography-spongy` instead of `jabit-cryptography-bouncy`. + Usage ----- @@ -53,6 +73,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 diff --git a/build.gradle b/build.gradle index 02a3903..0e96284 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,11 @@ subprojects { apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' + apply plugin: 'jacoco' sourceCompatibility = 1.7 group = 'ch.dissem.jabit' - version = '0.2.1-SNAPSHOT' + version = '1.1.0-SNAPSHOT' ext.isReleaseVersion = !version.endsWith("SNAPSHOT") @@ -34,7 +35,7 @@ subprojects { } signing { - required { isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives") } + required { isReleaseVersion && project.getProperties().get("signing.keyId")?.length() > 0 } sign configurations.archives } @@ -79,4 +80,13 @@ subprojects { } } } -} \ No newline at end of file + + jacocoTestReport { + reports { + xml.enabled = true + html.enabled = true + } + } + + check.dependsOn jacocoTestReport +} diff --git a/domain/build.gradle b/core/build.gradle similarity index 84% rename from domain/build.gradle rename to core/build.gradle index 1880c6b..8754cb3 100644 --- a/domain/build.gradle +++ b/core/build.gradle @@ -2,8 +2,8 @@ uploadArchives { repositories { mavenDeployer { pom.project { - name 'Jabit Domain' - artifactId = 'jabit-domain' + 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.' } } @@ -27,5 +27,5 @@ dependencies { compile 'org.slf4j:slf4j-api:1.7.12' testCompile 'junit:junit:4.11' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile project(':security-bc') + testCompile project(':cryptography-bc') } diff --git a/core/src/main/java/ch/dissem/bitmessage/BaseMessageCallback.java b/core/src/main/java/ch/dissem/bitmessage/BaseMessageCallback.java new file mode 100644 index 0000000..bf46b74 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/BaseMessageCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; + +/** + * Default implementation that doesn't do anything. + * + * @author Christian Basler + */ +public class BaseMessageCallback implements MessageCallback { + @Override + public void proofOfWorkStarted(ObjectPayload message) { + // No op + } + + @Override + public void proofOfWorkCompleted(ObjectPayload message) { + // No op + } + + @Override + public void messageOffered(ObjectPayload message, InventoryVector iv) { + // No op + } + + @Override + public void messageAcknowledged(InventoryVector iv) { + // No op + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java similarity index 53% rename from domain/src/main/java/ch/dissem/bitmessage/BitmessageContext.java rename to core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index 8603063..842611b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -16,30 +16,35 @@ 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.payload.*; +import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.payload.Broadcast; +import ch.dissem.bitmessage.entity.payload.Msg; +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.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.ApplicationException; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.utils.Property; +import ch.dissem.bitmessage.utils.TTL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; -import java.util.Arrays; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.*; +import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; +import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; import static ch.dissem.bitmessage.entity.Plaintext.Status.*; import static ch.dissem.bitmessage.entity.Plaintext.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.*; /** *

Use this class if you want to create a Bitmessage client.

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

+ * 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. + *

+ */ + public Builder pubkeyTTL(long days) { + if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days"); + TTL.pubkey(days); + return this; + } + public BitmessageContext build() { 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() { + messageCallback = new BaseMessageCallback(); + } + if (labeler == null) { + labeler = new DefaultLabeler(); + } + if (customCommandHandler == null) { + customCommandHandler = new CustomCommandHandler() { @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) { + public MessagePayload handle(CustomMessage request) { + throw new IllegalStateException( + "Received custom request, but no custom command handler configured."); } }; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java similarity index 62% rename from domain/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java rename to core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java index c409327..c8eed14 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java +++ b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java @@ -20,8 +20,9 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.ports.Labeler; import ch.dissem.bitmessage.ports.NetworkHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,10 +37,12 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; class DefaultMessageListener implements NetworkHandler.MessageListener { private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class); private final InternalContext ctx; + private final Labeler labeler; private final BitmessageContext.Listener listener; - public DefaultMessageListener(InternalContext context, BitmessageContext.Listener listener) { + public DefaultMessageListener(InternalContext context, Labeler labeler, BitmessageContext.Listener listener) { this.ctx = context; + this.labeler = labeler; this.listener = listener; } @@ -65,12 +68,15 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { receive(object, (Broadcast) payload); break; } + default: { + throw new IllegalArgumentException("Unknown payload type " + payload.getType()); + } } } protected void receive(ObjectMessage object, GetPubkey getPubkey) { - BitmessageAddress identity = ctx.getAddressRepo().findIdentity(getPubkey.getRipeTag()); - if (identity != null && identity.getPrivateKey() != null) { + BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag()); + if (identity != null && identity.getPrivateKey() != null && !identity.isChan()) { 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()); @@ -82,40 +88,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.info("Got pubkey for contact " + address); - ctx.getAddressRepo().save(address); - List 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, - 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(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()); Plaintext plaintext = msg.getPlaintext(); @@ -123,19 +131,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { if (!object.isSignatureValid(plaintext.getFrom().getPubkey())) { LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); } else { - plaintext.setStatus(RECEIVED); - plaintext.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD)); - plaintext.setInventoryVector(object.getInventoryVector()); - ctx.getMessageRepository().save(plaintext); - listener.receive(plaintext); - - if (identity.has(Pubkey.Feature.DOES_ACK)) { - ObjectMessage ack = plaintext.getAckMessage(); - if (ack != null) { - ctx.getInventory().storeObject(ack); - ctx.getNetworkHandler().offer(ack.getInventoryVector()); - } - } + receive(object.getInventoryVector(), msg.getPlaintext()); } break; } catch (DecryptionFailedException ignore) { @@ -145,7 +141,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; } @@ -154,14 +150,27 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) { LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); } else { - broadcast.getPlaintext().setStatus(RECEIVED); - broadcast.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)); - broadcast.getPlaintext().setInventoryVector(object.getInventoryVector()); - ctx.getMessageRepository().save(broadcast.getPlaintext()); - listener.receive(broadcast.getPlaintext()); + receive(object.getInventoryVector(), broadcast.getPlaintext()); } } catch (DecryptionFailedException ignore) { } } } + + protected void receive(InventoryVector iv, Plaintext msg) { + msg.setStatus(RECEIVED); + msg.setInventoryVector(iv); + labeler.setLabels(msg); + ctx.getMessageRepository().save(msg); + listener.receive(msg); + updatePubkey(msg.getFrom(), msg.getFrom().getPubkey()); + + if (msg.getType() == Type.MSG && identity.has(Pubkey.Feature.DOES_ACK)) { + ObjectMessage ack = plaintext.getAckMessage(); + if (ack != null) { + ctx.getInventory().storeObject(ack); + ctx.getNetworkHandler().offer(ack.getInventoryVector()); + } + } + } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/InternalContext.java b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java similarity index 63% rename from domain/src/main/java/ch/dissem/bitmessage/InternalContext.java rename to core/src/main/java/ch/dissem/bitmessage/InternalContext.java index 1614548..03c6bfc 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/InternalContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java @@ -16,16 +16,23 @@ package ch.dissem.bitmessage; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Encrypted; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.*; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.entity.*; import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TTL; import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Arrays; import java.util.TreeSet; import static ch.dissem.bitmessage.entity.Plaintext.Status.SENT; @@ -42,38 +49,45 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; public class InternalContext { private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class); - private final Security security; + public final static long NETWORK_NONCE_TRIALS_PER_BYTE = 1000; + public final static long NETWORK_EXTRA_BYTES = 1000; + + private final Cryptography cryptography; private final Inventory inventory; private final 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 final long clientNonce; - private final long networkNonceTrialsPerByte = 1000; - private final long networkExtraBytes = 1000; private long connectionTTL; private int connectionLimit; public InternalContext(BitmessageContext.Builder builder) { - this.security = builder.security; + 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; - Singleton.initialize(security); + Singleton.initialize(cryptography); // TODO: streams of new identities and subscriptions should also be added. This works only after a restart. for (BitmessageAddress address : addressRepository.getIdentities()) { @@ -86,7 +100,9 @@ public class InternalContext { streams.add(1L); } - init(security, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, proofOfWorkEngine); + init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, + proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, + messageCallback, customCommandHandler, builder.labeler); for (BitmessageAddress identity : addressRepository.getIdentities()) { streams.add(identity.getStream()); } @@ -100,8 +116,8 @@ public class InternalContext { } } - public Security getSecurity() { - return security; + public Cryptography getCryptography() { + return cryptography; } public Inventory getInventory() { @@ -116,7 +132,7 @@ public class InternalContext { return networkHandler; } - public AddressRepository getAddressRepo() { + public AddressRepository getAddressRepository() { return addressRepository; } @@ -124,10 +140,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; @@ -141,26 +165,8 @@ public class InternalContext { return port; } - public long getNetworkNonceTrialsPerByte() { - return networkNonceTrialsPerByte; - } - - public long getNonceTrialsPerByte(BitmessageAddress address) { - long nonceTrialsPerByte = address.getPubkey().getNonceTrialsPerByte(); - return networkNonceTrialsPerByte > nonceTrialsPerByte ? networkNonceTrialsPerByte : nonceTrialsPerByte; - } - - public long getNetworkExtraBytes() { - return networkExtraBytes; - } - - public long getExtraBytes(BitmessageAddress address) { - long extraBytes = address.getPubkey().getExtraBytes(); - return networkExtraBytes > extraBytes ? networkExtraBytes : extraBytes; - } - public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload, - final long timeToLive, final long nonceTrialsPerByte, final long extraBytes) { + final long timeToLive) { try { final BitmessageAddress recipient = (to != null ? to : from); long expires = UnixTime.now(+timeToLive); @@ -180,7 +186,7 @@ public class InternalContext { @Override public void onNonceCalculated(byte[] nonce) { object.encrypt(recipient.getPubkey()); - security.doProofOfWork(object, nonceTrialsPerByte, extraBytes, new ProofOfWorkCallback(object, payload)); + proofOfWorkService.doProofOfWork(to, object); } }); } else { @@ -189,16 +195,16 @@ public class InternalContext { } else if (payload instanceof Encrypted) { object.encrypt(recipient.getPubkey()); } - security.doProofOfWork(object, nonceTrialsPerByte, extraBytes, new ProofOfWorkCallback(object, payload)); + proofOfWorkService.doProofOfWork(to, object); } } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } public void sendPubkey(final BitmessageAddress identity, final long targetStream) { try { - long expires = UnixTime.now(+28 * DAY); + long expires = UnixTime.now(TTL.pubkey()); LOG.info("Expires at " + expires); final ObjectMessage response = new ObjectMessage.Builder() .stream(targetStream) @@ -206,45 +212,80 @@ public class InternalContext { .payload(identity.getPubkey()) .build(); response.sign(identity.getPrivateKey()); - response.encrypt(security.createPublicKey(identity.getPublicDecryptionKey())); + response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey())); messageCallback.proofOfWorkStarted(identity.getPubkey()); - security.doProofOfWork(response, networkNonceTrialsPerByte, networkExtraBytes, - new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] nonce) { - response.setNonce(nonce); - messageCallback.proofOfWorkCompleted(identity.getPubkey()); - inventory.storeObject(response); - networkHandler.offer(response.getInventoryVector()); - // TODO: save that the pubkey was just sent, and on which stream! - messageCallback.messageOffered(identity.getPubkey(), response.getInventoryVector()); - } - }); + // TODO: remember that the pubkey is just about to be sent, and on which stream! + proofOfWorkService.doProofOfWork(response); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } + /** + * Be aware that if the pubkey already exists in the inventory, the metods will not request it and the callback + * for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB. + */ public void requestPubkey(final BitmessageAddress contact) { - long expires = UnixTime.now(+2 * DAY); + BitmessageAddress stored = addressRepository.getAddress(contact.getAddress()); + + tryToFindMatchingPubkey(contact); + if (contact.getPubkey() != null) { + if (stored != null) { + stored.setPubkey(contact.getPubkey()); + addressRepository.save(stored); + } else { + addressRepository.save(contact); + } + return; + } + + if (stored == null) { + addressRepository.save(contact); + } + + long expires = UnixTime.now(TTL.getpubkey()); LOG.info("Expires at " + expires); - final ObjectMessage response = new ObjectMessage.Builder() + final ObjectMessage request = new ObjectMessage.Builder() .stream(contact.getStream()) .expiresTime(expires) .payload(new GetPubkey(contact)) .build(); - messageCallback.proofOfWorkStarted(response.getPayload()); - security.doProofOfWork(response, networkNonceTrialsPerByte, networkExtraBytes, - new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] nonce) { - response.setNonce(nonce); - messageCallback.proofOfWorkCompleted(response.getPayload()); - inventory.storeObject(response); - networkHandler.offer(response.getInventoryVector()); - messageCallback.messageOffered(response.getPayload(), response.getInventoryVector()); + messageCallback.proofOfWorkStarted(request.getPayload()); + proofOfWorkService.doProofOfWork(request); + } + + private void tryToFindMatchingPubkey(BitmessageAddress address) { + BitmessageAddress stored = addressRepository.getAddress(address.getAddress()); + if (stored != null) { + address.setAlias(stored.getAlias()); + address.setSubscribed(stored.isSubscribed()); + } + for (ObjectMessage object : inventory.getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) { + try { + Pubkey pubkey = (Pubkey) object.getPayload(); + if (address.getVersion() == 4) { + V4Pubkey v4Pubkey = (V4Pubkey) pubkey; + if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) { + v4Pubkey.decrypt(address.getPublicDecryptionKey()); + if (object.isSignatureValid(v4Pubkey)) { + address.setPubkey(v4Pubkey); + addressRepository.save(address); + break; + } else { + LOG.info("Found pubkey for " + address + " but signature is invalid"); + } } - }); + } else { + if (Arrays.equals(pubkey.getRipe(), address.getRipe())) { + address.setPubkey(pubkey); + addressRepository.save(address); + break; + } + } + } catch (Exception e) { + LOG.debug(e.getMessage(), e); + } + } } public long getClientNonce() { @@ -259,6 +300,10 @@ public class InternalContext { return connectionLimit; } + public CustomCommandHandler getCustomCommandHandler() { + return customCommandHandler; + } + public interface ContextHolder { void setContext(InternalContext context); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/MessageCallback.java b/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/MessageCallback.java rename to core/src/main/java/ch/dissem/bitmessage/MessageCallback.java diff --git a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java new file mode 100644 index 0000000..93e461f --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java @@ -0,0 +1,83 @@ +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.PlaintextHolder; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.ports.Cryptography; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; +import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalContext.ContextHolder { + private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class); + + private Cryptography cryptography; + private InternalContext ctx; + private ProofOfWorkRepository powRepo; + private MessageRepository messageRepo; + + public void doMissingProofOfWork() { + List<byte[]> items = powRepo.getItems(); + if (items.isEmpty()) return; + + LOG.info("Doing POW for " + items.size() + " tasks."); + for (byte[] initialHash : items) { + ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); + cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, this); + } + } + + public void doProofOfWork(ObjectMessage object) { + doProofOfWork(null, object); + } + + public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) { + Pubkey pubkey = recipient == null ? null : recipient.getPubkey(); + + long nonceTrialsPerByte = pubkey == null ? NETWORK_NONCE_TRIALS_PER_BYTE : pubkey.getNonceTrialsPerByte(); + long extraBytes = pubkey == null ? NETWORK_EXTRA_BYTES : pubkey.getExtraBytes(); + + powRepo.putObject(object, nonceTrialsPerByte, extraBytes); + if (object.getPayload() instanceof PlaintextHolder) { + Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext(); + plaintext.setInitialHash(cryptography.getInitialHash(object)); + messageRepo.save(plaintext); + } + cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this); + } + + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + ObjectMessage object = powRepo.getItem(initialHash).object; + object.setNonce(nonce); + Plaintext plaintext = messageRepo.getMessage(initialHash); + if (plaintext != null) { + plaintext.setInventoryVector(object.getInventoryVector()); + messageRepo.save(plaintext); + } + ctx.getInventory().storeObject(object); + powRepo.removeObject(initialHash); + ctx.getNetworkHandler().offer(object.getInventoryVector()); + } + + @Override + public void setContext(InternalContext ctx) { + this.ctx = ctx; + this.cryptography = security(); + this.powRepo = ctx.getProofOfWorkRepository(); + this.messageRepo = ctx.getMessageRepository(); + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java b/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java similarity index 93% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Addr.java index d00623e..6125910 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java @@ -29,6 +29,8 @@ import java.util.List; * The 'addr' command holds a list of known active Bitmessage nodes. */ public class Addr implements MessagePayload { + private static final long serialVersionUID = -5117688017050138720L; + private final List<NetworkAddress> addresses; private Addr(Builder builder) { @@ -53,10 +55,7 @@ public class Addr implements MessagePayload { } public static final class Builder { - private List<NetworkAddress> addresses = new ArrayList<NetworkAddress>(); - - public Builder() { - } + private List<NetworkAddress> addresses = new ArrayList<>(); public Builder addresses(Collection<NetworkAddress> addresses){ this.addresses.addAll(addresses); diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java b/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java similarity index 79% rename from domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java rename to core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java index 380e431..84b349d 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java @@ -20,6 +20,7 @@ import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.AccessCounter; import ch.dissem.bitmessage.utils.Base58; import ch.dissem.bitmessage.utils.Bytes; @@ -29,7 +30,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import static ch.dissem.bitmessage.utils.Decode.bytes; @@ -41,6 +44,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * holding private keys. */ public class BitmessageAddress implements Serializable { + private static final long serialVersionUID = 2386328540805994064L; + private final long version; private final long stream; private final byte[] ripe; @@ -57,6 +62,7 @@ public class BitmessageAddress implements Serializable { private String alias; private boolean subscribed; + private boolean chan; BitmessageAddress(long version, long stream, byte[] ripe) { try { @@ -84,15 +90,47 @@ public class BitmessageAddress implements Serializable { os.write(checksum, 0, 4); this.address = "BM-" + Base58.encode(os.toByteArray()); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } - BitmessageAddress(Pubkey publicKey) { + public BitmessageAddress(Pubkey publicKey) { this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe()); this.pubkey = publicKey; } + public BitmessageAddress(String address, String passphrase) { + this(address); + this.privateKey = new PrivateKey(this, passphrase); + this.pubkey = this.privateKey.getPubkey(); + if (!Arrays.equals(ripe, privateKey.getPubkey().getRipe())) { + throw new IllegalArgumentException("Wrong address or passphrase"); + } + } + + public static BitmessageAddress chan(String address, String passphrase) { + BitmessageAddress result = new BitmessageAddress(address, passphrase); + result.chan = true; + return result; + } + + public static BitmessageAddress chan(long stream, String passphrase) { + PrivateKey privateKey = new PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase); + BitmessageAddress result = new BitmessageAddress(privateKey); + result.chan = true; + return result; + } + + public static List<BitmessageAddress> deterministic(String passphrase, int numberOfAddresses, + long version, long stream, boolean shorter) { + List<BitmessageAddress> result = new ArrayList<>(numberOfAddresses); + List<PrivateKey> privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter); + for (PrivateKey pk : privateKeys) { + result.add(new BitmessageAddress(pk)); + } + return result; + } + public BitmessageAddress(PrivateKey privateKey) { this(privateKey.getPubkey()); this.privateKey = privateKey; @@ -125,7 +163,7 @@ public class BitmessageAddress implements Serializable { this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -137,7 +175,7 @@ public class BitmessageAddress implements Serializable { out.write(ripe); return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -188,7 +226,7 @@ public class BitmessageAddress implements Serializable { @Override public String toString() { - return alias != null ? alias : address; + return alias == null ? address : alias; } public byte[] getRipe() { @@ -222,6 +260,14 @@ public class BitmessageAddress implements Serializable { this.subscribed = subscribed; } + public boolean isChan() { + return chan; + } + + public void setChan(boolean chan) { + this.chan = chan; + } + public boolean has(Feature feature) { if (pubkey == null || feature == null) { return false; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java new file mode 100644 index 0000000..e43f56d --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.utils.AccessCounter; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.*; + +import static ch.dissem.bitmessage.utils.Decode.bytes; +import static ch.dissem.bitmessage.utils.Decode.varString; + +/** + * @author Christian Basler + */ +public class CustomMessage implements MessagePayload { + private static final long serialVersionUID = -8932056829480326011L; + + public static final String COMMAND_ERROR = "ERROR"; + + private final String command; + private final byte[] data; + + public CustomMessage(String command) { + this.command = command; + this.data = null; + } + + public CustomMessage(String command, byte[] data) { + this.command = command; + this.data = data; + } + + public static CustomMessage read(InputStream in, int length) throws IOException { + AccessCounter counter = new AccessCounter(); + return new CustomMessage(varString(in, counter), bytes(in, length - counter.length())); + } + + @Override + public Command getCommand() { + return Command.CUSTOM; + } + + public String getCustomCommand() { + return command; + } + + public byte[] getData() { + if (data != null) { + return data; + } else { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new ApplicationException(e); + } + } + } + + @Override + public void write(OutputStream out) throws IOException { + if (data != null) { + Encode.varString(command, out); + out.write(data); + } else { + throw new ApplicationException("Tried to write custom message without data. " + + "Programmer: did you forget to override #write()?"); + } + } + + public boolean isError() { + return COMMAND_ERROR.equals(command); + } + + public static CustomMessage error(String message) { + try { + return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java b/core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java similarity index 95% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java index 9eaa2ab..8b15371 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java @@ -17,7 +17,6 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.ports.Security; import java.io.IOException; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java b/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java similarity index 87% rename from domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java rename to core/src/main/java/ch/dissem/bitmessage/entity/GetData.java index b62e6e5..44fab5b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java @@ -28,6 +28,10 @@ import java.util.List; * The 'getdata' command is used to request objects from a node. */ public class GetData implements MessagePayload { + private static final long serialVersionUID = 1433878785969631061L; + + public static final int MAX_INVENTORY_SIZE = 50_000; + List<InventoryVector> inventory; private GetData(Builder builder) { @@ -44,19 +48,16 @@ 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); } } public static final class Builder { private List<InventoryVector> inventory = new LinkedList<>(); - public Builder() { - } - public Builder addInventoryVector(InventoryVector inventoryVector) { this.inventory.add(inventoryVector); return this; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java b/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java similarity index 90% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Inv.java index df1b380..fd2d40d 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java @@ -28,6 +28,8 @@ import java.util.List; * The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items. */ public class Inv implements MessagePayload { + private static final long serialVersionUID = 3662992522956947145L; + private List<InventoryVector> inventory; private Inv(Builder builder) { @@ -44,19 +46,16 @@ 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); } } public static final class Builder { private List<InventoryVector> inventory = new LinkedList<>(); - public Builder() { - } - public Builder addInventoryVector(InventoryVector inventoryVector) { this.inventory.add(inventoryVector); return this; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java similarity index 93% rename from domain/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java rename to core/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java index e6f6f0a..994952b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java @@ -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 } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java similarity index 94% rename from domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java rename to core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java index 8790d3a..347af6c 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java @@ -16,6 +16,7 @@ package ch.dissem.bitmessage.entity; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Encode; import java.io.ByteArrayOutputStream; @@ -32,6 +33,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * A network message is exchanged between two nodes. */ public class NetworkMessage implements Streamable { + private static final long serialVersionUID = 702708857104464809L; + /** * Magic value indicating message origin network, and used to seek to next message when stream state is unknown */ @@ -84,7 +87,7 @@ public class NetworkMessage implements Streamable { try { out.write(getChecksum(payloadBytes)); } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } // message payload diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java similarity index 94% rename from domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java rename to core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java index 128084e..68aa5ac 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java @@ -21,8 +21,8 @@ import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.ports.Security; import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Encode; @@ -36,6 +36,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * The 'object' command sends an object that is shared throughout the network. */ public class ObjectMessage implements MessagePayload { + private static final long serialVersionUID = 2495752480120659139L; + private byte[] nonce; private long expiresTime; private long objectType; @@ -111,7 +113,7 @@ public class ObjectMessage implements MessagePayload { payload.writeBytesToSign(out); return out.toByteArray(); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -145,7 +147,7 @@ public class ObjectMessage implements MessagePayload { ((Encrypted) payload).encrypt(publicKey.getEncryptionKey()); } } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -156,7 +158,11 @@ public class ObjectMessage implements MessagePayload { @Override public void write(OutputStream out) throws IOException { - out.write(nonce); + if (nonce == null) { + out.write(new byte[8]); + } else { + out.write(nonce); + } out.write(getPayloadBytesWithoutNonce()); } @@ -177,7 +183,7 @@ public class ObjectMessage implements MessagePayload { } return payloadBytes; } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -188,9 +194,6 @@ public class ObjectMessage implements MessagePayload { private long streamNumber; private ObjectPayload payload; - public Builder() { - } - public Builder nonce(byte[] nonce) { this.nonce = nonce; return this; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java similarity index 95% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 34ecd52..01deca8 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -19,6 +19,7 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; @@ -33,6 +34,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * The unencrypted message to be sent by 'msg' or 'broadcast'. */ public class Plaintext implements Streamable { + private static final long serialVersionUID = -5325729856394951079L; + private final Type type; private final BitmessageAddress from; private final long encoding; @@ -48,6 +51,7 @@ public class Plaintext implements Streamable { private Long received; private Set<Label> labels; + private byte[] initialHash; private Plaintext(Builder builder) { id = builder.id; @@ -120,9 +124,9 @@ public class Plaintext implements Streamable { public void setTo(BitmessageAddress to) { if (this.to.getVersion() != 0) - throw new RuntimeException("Correct address already set"); + throw new IllegalStateException("Correct address already set"); if (!Arrays.equals(this.to.getRipe(), to.getRipe())) { - throw new RuntimeException("RIPEs don't match"); + throw new IllegalArgumentException("RIPEs don't match"); } this.to = to; } @@ -239,7 +243,7 @@ public class Plaintext implements Streamable { } return text; } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -302,6 +306,14 @@ public class Plaintext implements Streamable { return ackMessage; } + public void setInitialHash(byte[] initialHash) { + this.initialHash = initialHash; + } + + public byte[] getInitialHash() { + return initialHash; + } + public enum Encoding { IGNORE(0), TRIVIAL(1), SIMPLE(2); @@ -436,7 +448,7 @@ public class Plaintext implements Streamable { this.encoding = Encoding.SIMPLE.getCode(); this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } return this; } @@ -495,7 +507,7 @@ public class Plaintext implements Streamable { behaviorBitfield )); } - if (to == null && type != Type.BROADCAST) { + if (to == null && type != Type.BROADCAST && destinationRipe != null) { to = new BitmessageAddress(0, 0, destinationRipe); } if (type == Type.MSG && ackMessage == null && ackData == null) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java b/core/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java rename to core/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java b/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java b/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java similarity index 93% rename from domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java rename to core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java index 1aad501..815a20f 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java @@ -23,6 +23,8 @@ import java.io.OutputStream; * The 'verack' command answers a 'version' command, accepting the other node's version. */ public class VerAck implements MessagePayload { + private static final long serialVersionUID = -4302074845199181687L; + @Override public Command getCommand() { return Command.VERACK; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Version.java b/core/src/main/java/ch/dissem/bitmessage/entity/Version.java similarity index 98% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Version.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Version.java index 70c2ad2..3722528 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Version.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Version.java @@ -29,6 +29,8 @@ import java.util.Random; * The 'version' command advertises this node's latest supported protocol version upon initiation. */ public class Version implements MessagePayload { + private static final long serialVersionUID = 7219240857343176567L; + /** * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's * version is lower but continue with the connection if it is higher. @@ -143,9 +145,6 @@ public class Version implements MessagePayload { private String userAgent; private long[] streamNumbers; - public Builder() { - } - public Builder defaults() { version = BitmessageContext.CURRENT_VERSION; services = 1; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java similarity index 97% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java index bf5ff7f..7e19e8f 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java @@ -21,7 +21,6 @@ import ch.dissem.bitmessage.entity.Encrypted; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.PlaintextHolder; import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.ports.Security; import java.io.IOException; @@ -33,6 +32,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * Broadcasts are version 4 or 5. */ public abstract class Broadcast extends ObjectPayload implements Encrypted, PlaintextHolder { + private static final long serialVersionUID = 4064521827582239069L; + protected final long stream; protected CryptoBox encrypted; protected Plaintext plaintext; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java similarity index 95% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java index a870b32..6609070 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.entity.payload; import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.utils.*; import org.slf4j.Logger; @@ -30,6 +31,7 @@ import static ch.dissem.bitmessage.utils.Singleton.security; public class CryptoBox implements Streamable { + private static final long serialVersionUID = 7217659539975573852L; private static final Logger LOG = LoggerFactory.getLogger(CryptoBox.class); private final byte[] initializationVector; @@ -38,7 +40,12 @@ public class CryptoBox implements Streamable { private final byte[] mac; private byte[] encrypted; + public CryptoBox(Streamable data, byte[] K) throws IOException { + this(Encode.bytes(data), K); + } + + public CryptoBox(byte[] data, byte[] K) throws IOException { curveType = 0x02CA; // 1. The destination public key is called K. @@ -58,7 +65,7 @@ public class CryptoBox implements Streamable { byte[] key_m = Arrays.copyOfRange(H, 32, 64); // 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7. // 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text. - encrypted = security().crypt(true, Encode.bytes(data), key_e, initializationVector); + encrypted = security().crypt(true, data, key_e, initializationVector); // 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. mac = calculateMac(key_m); @@ -118,7 +125,7 @@ public class CryptoBox implements Streamable { writeWithoutMAC(macData); return security().mac(key_m, macData.toByteArray()); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java index 7051814..66cc296 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java @@ -28,6 +28,8 @@ import java.util.Arrays; * have to know what it is. */ public class GenericPayload extends ObjectPayload { + private static final long serialVersionUID = -912314085064185940L; + private long stream; private byte[] data; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java index e31dbe3..06e623a 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java @@ -27,6 +27,8 @@ import java.io.OutputStream; * Request for a public key. */ public class GetPubkey extends ObjectPayload { + private static final long serialVersionUID = -3634516646972610180L; + private long stream; private byte[] ripeTag; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java similarity index 97% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java index 52c36e7..8974ce3 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java @@ -31,6 +31,8 @@ import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; * Used for person-to-person messages. */ public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder { + private static final long serialVersionUID = 4327495048296365733L; + private long stream; private CryptoBox encrypted; private Plaintext plaintext; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java index 0ca45cd..33da28d 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java @@ -21,12 +21,13 @@ 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. */ public abstract class ObjectPayload implements Streamable { + private static final long serialVersionUID = -5034977402902364482L; + private final long version; protected ObjectPayload(long version) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java similarity index 97% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java index 675bc3d..722f60c 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java @@ -26,6 +26,8 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * Public keys for signing and encryption, the answer to a 'getpubkey' request. */ public abstract class Pubkey extends ObjectPayload { + private static final long serialVersionUID = -6634533361454999619L; + public final static long LATEST_VERSION = 4; protected Pubkey(long version) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java similarity index 97% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java index da16929..0eceb91 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java @@ -27,6 +27,8 @@ import java.io.OutputStream; * A version 2 public key. */ public class V2Pubkey extends Pubkey { + private static final long serialVersionUID = -257598690676510460L; + protected long stream; protected int behaviorBitfield; protected byte[] publicSigningKey; // 64 Bytes @@ -96,9 +98,6 @@ public class V2Pubkey extends Pubkey { private byte[] publicSigningKey; private byte[] publicEncryptionKey; - public Builder() { - } - public Builder stream(long streamNumber) { this.streamNumber = streamNumber; return this; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java similarity index 98% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java index bf91afe..72358d0 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java @@ -29,6 +29,8 @@ import java.util.Objects; * A version 3 public key. */ public class V3Pubkey extends V2Pubkey { + private static final long serialVersionUID = 6958853116648528319L; + long nonceTrialsPerByte; long extraBytes; byte[] signature; @@ -123,9 +125,6 @@ public class V3Pubkey extends V2Pubkey { private long extraBytes; private byte[] signature = new byte[0]; - public Builder() { - } - public Builder stream(long streamNumber) { this.streamNumber = streamNumber; return this; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java index 03364d2..7781455 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java @@ -18,7 +18,6 @@ package ch.dissem.bitmessage.entity.payload; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.ports.Security; import java.io.IOException; import java.io.InputStream; @@ -29,6 +28,8 @@ import java.io.OutputStream; * Broadcasts are version 4 or 5. */ public class V4Broadcast extends Broadcast { + private static final long serialVersionUID = 195663108282762711L; + protected V4Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) { super(version, stream, encrypted, plaintext); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java similarity index 98% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java index 048e7a6..8aa0ee4 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java @@ -33,6 +33,8 @@ import java.util.Arrays; * to create messages to be used in spam or in flooding attacks. */ public class V4Pubkey extends Pubkey implements Encrypted { + private static final long serialVersionUID = 1556710353694033093L; + private long stream; private byte[] tag; private CryptoBox encrypted; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java rename to core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java index c12aae0..8f07a30 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java @@ -28,6 +28,8 @@ import java.io.OutputStream; * Users who are subscribed to the sending address will see the message appear in their inbox. */ public class V5Broadcast extends V4Broadcast { + private static final long serialVersionUID = 920649721626968644L; + private byte[] tag; private V5Broadcast(long stream, byte[] tag, CryptoBox encrypted) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java similarity index 92% rename from domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java rename to core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java index f87dd13..127b90a 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java @@ -25,6 +25,8 @@ import java.io.Serializable; import java.util.Arrays; public class InventoryVector implements Streamable, Serializable { + private static final long serialVersionUID = -7349009673063348719L; + /** * Hash of the object */ @@ -38,12 +40,11 @@ public class InventoryVector implements Streamable, Serializable { InventoryVector that = (InventoryVector) o; return Arrays.equals(hash, that.hash); - } @Override public int hashCode() { - return hash != null ? Arrays.hashCode(hash) : 0; + return hash == null ? 0 : Arrays.hashCode(hash); } public byte[] getHash() { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java rename to core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java index 02f0384..facbe55 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java @@ -20,6 +20,8 @@ import java.io.Serializable; import java.util.Objects; public class Label implements Serializable { + private static final long serialVersionUID = 831782893630994914L; + private Object id; private String label; private Type type; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java similarity index 97% rename from domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java rename to core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java index 794ae44..d0324a4 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.entity.valueobject; import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.UnixTime; @@ -30,6 +31,8 @@ import java.util.Arrays; * A node's address. It's written in IPv6 format. */ public class NetworkAddress implements Streamable { + private static final long serialVersionUID = 2500120578167100300L; + private long time; /** @@ -85,7 +88,7 @@ public class NetworkAddress implements Streamable { try { return InetAddress.getByAddress(ipv6); } catch (UnknownHostException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -133,9 +136,6 @@ public class NetworkAddress implements Streamable { private byte[] ipv6; private int port; - public Builder() { - } - public Builder time(final long time) { this.time = time; return this; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java similarity index 56% rename from domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java rename to core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java index d07c859..b9c3afb 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java @@ -16,14 +16,19 @@ package ch.dissem.bitmessage.entity.valueobject; +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.ApplicationException; 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 java.io.*; +import java.util.ArrayList; +import java.util.List; import static ch.dissem.bitmessage.utils.Singleton.security; @@ -32,7 +37,10 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * {@link Pubkey} object. */ public class PrivateKey implements Streamable { + private static final long serialVersionUID = 8562555470709110558L; + public static final int PRIVATE_KEY_SIZE = 32; + private final byte[] privateSigningKey; private final byte[] privateEncryptionKey; @@ -63,16 +71,78 @@ public class PrivateKey implements Streamable { this.pubkey = pubkey; } - 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, - nonceTrialsPerByte, extraBytes, features); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + public PrivateKey(BitmessageAddress address, String passphrase) { + this(address.getVersion(), address.getStream(), passphrase); + } + + public PrivateKey(long version, long stream, String passphrase) { + this(new Builder(version, stream, false).seed(passphrase).generate()); + } + + private PrivateKey(Builder builder) { + this.privateSigningKey = builder.privSK; + this.privateEncryptionKey = builder.privEK; + this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK, builder.pubEK, + InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES); + } + + private static class Builder { + final long version; + final long stream; + final boolean shorter; + + byte[] seed; + long nextNonce; + + byte[] privSK, privEK; + byte[] pubSK, pubEK; + + private Builder(long version, long stream, boolean shorter) { + this.version = version; + this.stream = stream; + this.shorter = shorter; } + + Builder seed(String passphrase) { + try { + seed = passphrase.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + return this; + } + + Builder generate() { + try { + long signingKeyNonce = nextNonce; + long encryptionKeyNonce = nextNonce + 1; + byte[] ripe; + do { + privEK = Bytes.truncate(security().sha512(seed, Encode.varInt(encryptionKeyNonce)), 32); + privSK = Bytes.truncate(security().sha512(seed, Encode.varInt(signingKeyNonce)), 32); + pubSK = security().createPublicKey(privSK); + pubEK = security().createPublicKey(privEK); + ripe = security().ripemd160(security().sha512(pubSK, pubEK)); + + signingKeyNonce += 2; + encryptionKeyNonce += 2; + } while (ripe[0] != 0 || (shorter && ripe[1] != 0)); + nextNonce = signingKeyNonce; + } catch (IOException e) { + throw new ApplicationException(e); + } + return this; + } + } + + public static List<PrivateKey> deterministic(String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) { + List<PrivateKey> result = new ArrayList<>(numberOfAddresses); + Builder builder = new Builder(version, stream, shorter).seed(passphrase); + for (int i = 0; i < numberOfAddresses; i++) { + builder.generate(); + result.add(new PrivateKey(builder)); + } + return result; } public static PrivateKey read(InputStream is) throws IOException { diff --git a/domain/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java b/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java similarity index 92% rename from domain/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java index 7da990a..9f6674d 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java @@ -20,6 +20,8 @@ package ch.dissem.bitmessage.exception; * Indicates an illegal Bitmessage address */ public class AddressFormatException extends RuntimeException { + private static final long serialVersionUID = 6943764578672021573L; + public AddressFormatException(String message) { super(message); } diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java b/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java new file mode 100644 index 0000000..e688cb1 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.exception; + +/** + * @author Christian Basler + */ +public class ApplicationException extends RuntimeException { + private static final long serialVersionUID = 1796776684126759324L; + + public ApplicationException(Throwable cause) { + super(cause); + } + + public ApplicationException(String message) { + super(message); + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java b/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java similarity index 90% rename from domain/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java index c68a050..c9e3efd 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java @@ -17,4 +17,5 @@ package ch.dissem.bitmessage.exception; public class DecryptionFailedException extends Exception { + private static final long serialVersionUID = 3241116253113872731L; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java b/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java similarity index 93% rename from domain/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java index 685e710..94a5d84 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.util.Arrays; public class InsufficientProofOfWorkException extends IOException { + private static final long serialVersionUID = 9105580366564571318L; + public InsufficientProofOfWorkException(byte[] target, byte[] hash) { super("Insufficient proof of work: " + Strings.hex(target) + " required, " + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved."); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/exception/NodeException.java b/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java similarity index 93% rename from domain/src/main/java/ch/dissem/bitmessage/exception/NodeException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java index 9ab2b7f..cecd950 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/exception/NodeException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java @@ -22,6 +22,8 @@ package ch.dissem.bitmessage.exception; * @author Ch. Basler */ public class NodeException extends RuntimeException { + private static final long serialVersionUID = 2965325796118227802L; + public NodeException(String message) { super(message); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java b/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java similarity index 98% rename from domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java rename to core/src/main/java/ch/dissem/bitmessage/factory/Factory.java index 135dd93..48cafff 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -196,7 +196,8 @@ public class Factory { } } - public static ObjectPayload getBroadcast(BitmessageAddress sendingAddress, Plaintext plaintext) { + public static Broadcast getBroadcast(Plaintext plaintext) { + BitmessageAddress sendingAddress = plaintext.getFrom(); if (sendingAddress.getVersion() < 4) { return new V4Broadcast(sendingAddress, plaintext); } else { diff --git a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java similarity index 94% rename from domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java rename to core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java index 8dca6d2..d13e73e 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -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); @@ -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"); diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/AbstractSecurity.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java similarity index 75% rename from domain/src/main/java/ch/dissem/bitmessage/ports/AbstractSecurity.java rename to core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java index bd55180..3b08377 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/AbstractSecurity.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java @@ -19,6 +19,7 @@ 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.ApplicationException; import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Bytes; @@ -34,18 +35,24 @@ import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; +import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; +import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; +import static ch.dissem.bitmessage.utils.Numbers.max; + /** * Implements everything that isn't directly dependent on either Spongy- or Bouncycastle. */ -public abstract class AbstractSecurity implements Security, InternalContext.ContextHolder { - public static final Logger LOG = LoggerFactory.getLogger(Security.class); +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 AbstractSecurity(String provider) { + protected AbstractCryptography(String provider) { this.provider = provider; } @@ -94,18 +101,14 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, ProofOfWorkEngine.Callback callback) { - try { - if (nonceTrialsPerByte < 1000) nonceTrialsPerByte = 1000; - if (extraBytes < 1000) extraBytes = 1000; + nonceTrialsPerByte = max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE); + extraBytes = max(extraBytes, NETWORK_EXTRA_BYTES); - byte[] initialHash = getInitialHash(object); + byte[] initialHash = getInitialHash(object); - byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); + byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); - context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback); - } catch (IOException e) { - throw new RuntimeException(e); - } + context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback); } public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) @@ -117,15 +120,25 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont } } - private byte[] getInitialHash(ObjectMessage object) throws IOException { + @Override + public byte[] getInitialHash(ObjectMessage object) { return sha512(object.getPayloadBytesWithoutNonce()); } - private byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException { + @Override + public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + if (nonceTrialsPerByte == 0) nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE; + if (extraBytes == 0) extraBytes = NETWORK_EXTRA_BYTES; + BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now()); - BigInteger numerator = TWO.pow(64); + BigInteger numerator = TWO_POW_64; BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes); - BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte).multiply(powLength.add(powLength.multiply(TTL).divide(BigInteger.valueOf(2).pow(16)))); + BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte) + .multiply( + powLength.add( + powLength.multiply(TTL).divide(TWO_POW_16) + ) + ); return Bytes.expand(numerator.divide(denominator).toByteArray(), 8); } @@ -141,7 +154,7 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont try { return MessageDigest.getInstance(algorithm, provider); } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -151,7 +164,7 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont mac.init(new SecretKeySpec(key_m, "HmacSHA256")); return mac.doFinal(data); } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java similarity index 92% rename from domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java rename to core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java index 2770997..ff397ba 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java @@ -30,12 +30,17 @@ public interface AddressRepository { */ List<BitmessageAddress> getIdentities(); + /** + * @return all subscribed chans. + */ + List<BitmessageAddress> getChans(); + List<BitmessageAddress> getSubscriptions(); List<BitmessageAddress> getSubscriptions(long broadcastVersion); /** - * @return all Bitmessage addresses that have no private key. + * @return all Bitmessage addresses that have no private key or are chans. */ List<BitmessageAddress> getContacts(); diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/Security.java b/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java similarity index 97% rename from domain/src/main/java/ch/dissem/bitmessage/ports/Security.java rename to core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java index c5fcb8f..48739ea 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/Security.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java @@ -29,7 +29,7 @@ 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 Security { +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 @@ -134,6 +134,10 @@ public interface Security { void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException; + byte[] getInitialHash(ObjectMessage object); + + byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes); + /** * Calculates the MAC for a message (data) * diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java b/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java new file mode 100644 index 0000000..8e49586 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.MessagePayload; + +/** + * @author Christian Basler + */ +public interface CustomCommandHandler { + MessagePayload handle(CustomMessage request); +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java b/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java new file mode 100644 index 0000000..1598ce1 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; + +import java.util.Iterator; + +public class DefaultLabeler implements Labeler, InternalContext.ContextHolder { + private InternalContext ctx; + + @Override + public void setLabels(Plaintext msg) { + if (msg.getType() == Plaintext.Type.BROADCAST) { + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)); + } else { + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD)); + } + } + + @Override + public void markAsRead(Plaintext msg) { + Iterator<Label> iterator = msg.getLabels().iterator(); + while (iterator.hasNext()) { + Label label = iterator.next(); + if (label.getType() == Label.Type.UNREAD) { + iterator.remove(); + } + } + } + + @Override + public void markAsUnread(Plaintext msg) { + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.UNREAD)); + } + + @Override + public void delete(Plaintext msg) { + msg.getLabels().clear(); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.TRASH)); + } + + @Override + public void archive(Plaintext msg) { + msg.getLabels().clear(); + } + + @Override + public void setContext(InternalContext ctx) { + this.ctx = ctx; + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java b/core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java rename to core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java b/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java new file mode 100644 index 0000000..95febaf --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.entity.Plaintext; + +/** + * Defines and sets labels + */ +public interface Labeler { + /** + * Sets the labels of a newly received message. + * + * @param msg an unlabeled message or broadcast + */ + void setLabels(Plaintext msg); + + void markAsRead(Plaintext msg); + + void markAsUnread(Plaintext msg); + + void delete(Plaintext msg); + + void archive(Plaintext msg); +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java b/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java similarity index 98% rename from domain/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java rename to core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java index 8d43423..c68a474 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +72,7 @@ public class MemoryNodeRegistry implements NodeRegistry { } } } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java similarity index 96% rename from domain/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java rename to core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java index af7b2bc..9e949a7 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java @@ -30,6 +30,8 @@ public interface MessageRepository { int countUnread(Label label); + Plaintext getMessage(byte[] initialHash); + List<Plaintext> findMessages(Label label); List<Plaintext> findMessages(Status status); diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java similarity index 86% rename from domain/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java rename to core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java index ac65d3d..50164a3 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java @@ -16,6 +16,7 @@ package ch.dissem.bitmessage.ports; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +50,7 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { try { semaphore.acquire(); } catch (InterruptedException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } callback = new CallbackWrapper(callback); int cores = Runtime.getRuntime().availableProcessors(); @@ -88,7 +89,7 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { mda = MessageDigest.getInstance("SHA-512"); } catch (NoSuchAlgorithmException e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -101,14 +102,12 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) { synchronized (callback) { if (!Thread.interrupted()) { - try { - callback.onNonceCalculated(nonce); - } finally { - semaphore.release(); - for (Worker w : workers) { - w.interrupt(); - } + for (Worker w : workers) { + w.interrupt(); } + // Clear interrupted flag for callback + Thread.interrupted(); + callback.onNonceCalculated(initialHash, nonce); } } return; @@ -128,12 +127,14 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { } @Override - public void onNonceCalculated(byte[] nonce) { + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + // Prevents the callback from being called twice if two nonces are found simultaneously synchronized (this) { if (waiting) { + semaphore.release(); LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds"); waiting = false; - callback.onNonceCalculated(nonce); + callback.onNonceCalculated(initialHash, nonce); } } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java b/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java similarity index 74% rename from domain/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java rename to core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java index e2fd170..909d3dd 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java @@ -16,6 +16,7 @@ package ch.dissem.bitmessage.ports; +import ch.dissem.bitmessage.entity.CustomMessage; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.utils.Property; @@ -34,7 +35,18 @@ public interface NetworkHandler { * An implementation should disconnect if either the timeout is reached or the returned thread is interrupted. * </p> */ - Future<?> synchronize(InetAddress trustedHost, int port, MessageListener listener, long timeoutInSeconds); + Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds); + + /** + * Send a custom message to a specific node (that should implement handling for this message type) and returns + * the response, which in turn is expected to be a {@link CustomMessage}. + * + * @param server the node's address + * @param port the node's port + * @param request the request + * @return the response + */ + CustomMessage send(InetAddress server, int port, CustomMessage request); /** * Start a full network node, accepting incoming connections and relaying objects. diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java b/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java rename to core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java similarity index 95% rename from domain/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java rename to core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java index 90513dc..fc7b4c2 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java @@ -35,6 +35,6 @@ public interface ProofOfWorkEngine { /** * @param nonce 8 bytes nonce */ - void onNonceCalculated(byte[] nonce); + void onNonceCalculated(byte[] initialHash, byte[] nonce); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java new file mode 100644 index 0000000..739c172 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java @@ -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; + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java similarity index 60% rename from domain/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java rename to core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java index 25d51aa..a7d0d57 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java @@ -16,30 +16,35 @@ package ch.dissem.bitmessage.ports; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Bytes; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import static ch.dissem.bitmessage.utils.Bytes.inc; /** * 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 void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { - byte[] nonce = new byte[8]; - MessageDigest mda; try { - mda = MessageDigest.getInstance("SHA-512"); - } catch (Exception e) { - throw new RuntimeException(e); + MessageDigest mda = MessageDigest.getInstance("SHA-512"); + byte[] nonce = new byte[8]; + do { + inc(nonce); + mda.update(nonce); + mda.update(initialHash); + } while (Bytes.lt(target, mda.digest(mda.digest()), 8)); + callback.onNonceCalculated(initialHash, nonce); + } catch (NoSuchAlgorithmException e) { + throw new ApplicationException(e); } - do { - inc(nonce); - mda.update(nonce); - mda.update(initialHash); - } while (Bytes.lt(target, mda.digest(mda.digest()), 8)); - callback.onNonceCalculated(nonce); } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java b/core/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java rename to core/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java b/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java similarity index 86% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Base58.java index ec2476a..a67e344 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.exception.AddressFormatException; +import ch.dissem.bitmessage.exception.ApplicationException; import java.io.UnsupportedEncodingException; @@ -30,7 +31,7 @@ import static java.util.Arrays.copyOfRange; */ public class Base58 { private static final int[] INDEXES = new int[128]; - private static char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); static { for (int i = 0; i < INDEXES.length; i++) { @@ -44,27 +45,27 @@ public class Base58 { /** * Encodes the given bytes in base58. No checksum is appended. * - * @param input to encode + * @param data to encode * @return base58 encoded input */ - public static String encode(byte[] input) { - if (input.length == 0) { + public static String encode(byte[] data) { + if (data.length == 0) { return ""; } - input = copyOfRange(input, 0, input.length); + final byte[] bytes = copyOfRange(data, 0, data.length); // Count leading zeroes. int zeroCount = 0; - while (zeroCount < input.length && input[zeroCount] == 0) { + while (zeroCount < bytes.length && bytes[zeroCount] == 0) { ++zeroCount; } // The actual encoding. - byte[] temp = new byte[input.length * 2]; + byte[] temp = new byte[bytes.length * 2]; int j = temp.length; int startAt = zeroCount; - while (startAt < input.length) { - byte mod = divmod58(input, startAt); - if (input[startAt] == 0) { + while (startAt < bytes.length) { + byte mod = divmod58(bytes, startAt); + if (bytes[startAt] == 0) { ++startAt; } temp[--j] = (byte) ALPHABET[mod]; @@ -83,7 +84,7 @@ public class Base58 { try { return new String(output, "US-ASCII"); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // Cannot happen. + throw new ApplicationException(e); // Cannot happen. } } @@ -97,7 +98,7 @@ public class Base58 { char c = input.charAt(i); int digit58 = -1; - if (c >= 0 && c < 128) { + if (c < 128) { digit58 = INDEXES[c]; } if (digit58 < 0) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java b/core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java rename to core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Collections.java b/core/src/main/java/ch/dissem/bitmessage/utils/Collections.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Collections.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Collections.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.java b/core/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.java rename to core/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java b/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java similarity index 94% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Decode.java index b539aa9..47b0ee3 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java @@ -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"); } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java b/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java similarity index 72% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Encode.java index a78d03f..f5eac43 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java @@ -41,6 +41,34 @@ public class Encode { varInt(value, stream, null); } + public static byte[] varInt(long value) throws IOException { + final byte[] result; + if (value < 0) { + // This is due to the fact that Java doesn't really support unsigned values. + // Please be aware that this might be an error due to a smaller negative value being cast to long. + // Normally, negative values shouldn't occur within the protocol, and I large enough longs + // to being recognized as negatives aren't realistic. + ByteBuffer buffer = ByteBuffer.allocate(9); + buffer.put((byte) 0xff); + result = buffer.putLong(value).array(); + } else if (value < 0xfd) { + result = new byte[]{(byte) value}; + } else if (value <= 0xffffL) { + ByteBuffer buffer = ByteBuffer.allocate(3); + buffer.put((byte) 0xfd); + result = buffer.putShort((short) value).array(); + } else if (value <= 0xffffffffL) { + ByteBuffer buffer = ByteBuffer.allocate(5); + buffer.put((byte) 0xfe); + result = buffer.putInt((int) value).array(); + } else { + ByteBuffer buffer = ByteBuffer.allocate(9); + buffer.put((byte) 0xff); + result = buffer.putLong(value).array(); + } + return result; + } + public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException { if (value < 0) { // This is due to the fact that Java doesn't really support unsigned values. @@ -81,7 +109,7 @@ public class Encode { } public static void int16(long value, OutputStream stream, AccessCounter counter) throws IOException { - stream.write(ByteBuffer.allocate(4).putInt((int) value).array(), 2, 2); + stream.write(ByteBuffer.allocate(2).putShort((short) value).array()); inc(counter, 2); } @@ -103,15 +131,23 @@ public class Encode { inc(counter, 8); } - public static void varString(String value, OutputStream stream) throws IOException { + public static void varString(String value, OutputStream out) throws IOException { byte[] bytes = value.getBytes("utf-8"); - // FIXME: technically, it says the length in characters, but I think this one might be correct + // Technically, it says the length in characters, but I think this one might be correct. + // It doesn't really matter, as only ASCII characters are being used. // see also Decode#varString() - varInt(bytes.length, stream); - stream.write(bytes); + varInt(bytes.length, out); + out.write(bytes); + } + + public static void varBytes(byte[] data, OutputStream out) throws IOException { + varInt(data.length, out); + out.write(data); } /** + * Serializes a {@link Streamable} object and returns the byte array. + * * @param streamable the object to be serialized * @return an array of bytes representing the given streamable object. * @throws IOException if an I/O error occurs. diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java b/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java new file mode 100644 index 0000000..9d0c078 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java @@ -0,0 +1,10 @@ +package ch.dissem.bitmessage.utils; + +/** + * @author Christian Basler + */ +public class Numbers { + public static long max(long a, long b) { + return a > b ? a : b; + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Points.java b/core/src/main/java/ch/dissem/bitmessage/utils/Points.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Points.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Points.java diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Property.java b/core/src/main/java/ch/dissem/bitmessage/utils/Property.java similarity index 80% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Property.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Property.java index e00d193..b823eb5 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Property.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Property.java @@ -16,6 +16,9 @@ package ch.dissem.bitmessage.utils; +import java.util.Arrays; +import java.util.Objects; + /** * Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now * used to contain different status information. It is by default displayed in some JSON inspired human readable @@ -43,12 +46,19 @@ public class Property { return value; } - public Property getProperty(String name) { + /** + * Returns the property if available or <code>null</code> otherwise. + * Subproperties can be requested by submitting the sequence of properties. + */ + public Property getProperty(String... name) { + if (name == null || name.length == 0) return null; + for (Property p : properties) { - if (name == null) { - if (p.name == null) return p; - } else { - if (name.equals(p.name)) return p; + 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; diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Singleton.java b/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java similarity index 68% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Singleton.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java index d272beb..a751c65 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Singleton.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java @@ -16,23 +16,21 @@ package ch.dissem.bitmessage.utils; -import ch.dissem.bitmessage.ports.Security; +import ch.dissem.bitmessage.ports.Cryptography; /** - * Created by chris on 20.07.15. + * @author Christian Basler */ public class Singleton { - private static Security security; + private static Cryptography cryptography; - public static void initialize(Security security) { + public static void initialize(Cryptography cryptography) { synchronized (Singleton.class) { - if (Singleton.security == null) { - Singleton.security = security; - } + Singleton.cryptography = cryptography; } } - public static Security security() { - return security; + public static Cryptography security() { + return cryptography; } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Strings.java b/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java similarity index 95% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Strings.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Strings.java index 5c1aae9..7c9e13f 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Strings.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java @@ -16,8 +16,6 @@ package ch.dissem.bitmessage.utils; -import ch.dissem.bitmessage.entity.payload.ObjectType; - /** * Some utilities to handle strings. * TODO: Probably this should be split in a GUI related and an SQL related utility class. diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java new file mode 100644 index 0000000..17e31e5 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java @@ -0,0 +1,40 @@ +package ch.dissem.bitmessage.utils; + +import static ch.dissem.bitmessage.utils.UnixTime.DAY; + +/** + * Stores times to live for different object types. Usually this shouldn't be messed with, + * but for tests it might be a good idea to reduce it to a minimum, and on mobile clients + * you might want to optimize it as well. + * + * @author Christian Basler + */ +public class TTL { + private static long msg = 2 * DAY; + private static long getpubkey = 2 * DAY; + private static long pubkey = 28 * DAY; + + public static long msg() { + return msg; + } + + public static void msg(long msg) { + TTL.msg = msg; + } + + public static long getpubkey() { + return getpubkey; + } + + public static void getpubkey(long getpubkey) { + TTL.getpubkey = getpubkey; + } + + public static long pubkey() { + return pubkey; + } + + public static void pubkey(long pubkey) { + TTL.pubkey = pubkey; + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java b/core/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java similarity index 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java rename to core/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java diff --git a/domain/src/main/resources/nodes.txt b/core/src/main/resources/nodes.txt similarity index 100% rename from domain/src/main/resources/nodes.txt rename to core/src/main/resources/nodes.txt diff --git a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java new file mode 100644 index 0000000..7ca0d87 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java @@ -0,0 +1,257 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.ObjectType; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.utils.MessageMatchers; +import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TestUtils; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +import static ch.dissem.bitmessage.entity.payload.ObjectType.*; +import static ch.dissem.bitmessage.utils.MessageMatchers.object; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Christian Basler + */ +public class BitmessageContextTest { + private BitmessageContext ctx; + private BitmessageContext.Listener listener; + + @Before + public void setUp() throws Exception { + Singleton.initialize(null); + listener = mock(BitmessageContext.Listener.class); + ctx = new BitmessageContext.Builder() + .addressRepo(mock(AddressRepository.class)) + .cryptography(new BouncyCryptography()) + .inventory(mock(Inventory.class)) + .listener(listener) + .messageCallback(mock(MessageCallback.class)) + .messageRepo(mock(MessageRepository.class)) + .networkHandler(mock(NetworkHandler.class)) + .nodeRegistry(mock(NodeRegistry.class)) + .powRepo(mock(ProofOfWorkRepository.class)) + .proofOfWorkEngine(mock(ProofOfWorkEngine.class)) + .build(); + } + + @Test + public void ensureContactIsSavedAndPubkeyRequested() { + BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); + ctx.addContact(contact); + + verify(ctx.addresses(), times(2)).save(contact); + verify(ctx.internals().getProofOfWorkEngine()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + } + + @Test + public void ensurePubkeyIsNotRequestedIfItExists() throws Exception { + ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); + Pubkey pubkey = (Pubkey) object.getPayload(); + BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); + contact.setPubkey(pubkey); + + ctx.addContact(contact); + + verify(ctx.addresses(), times(1)).save(contact); + verify(ctx.internals().getProofOfWorkEngine(), never()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + } + + @Test + public void ensureV2PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { + BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); + when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class))) + .thenReturn(Collections.singletonList( + TestUtils.loadObjectMessage(2, "V2Pubkey.payload") + )); + + ctx.addContact(contact); + + verify(ctx.addresses(), atLeastOnce()).save(contact); + verify(ctx.internals().getProofOfWorkEngine(), never()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + } + + @Test + public void ensureV4PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { + BitmessageAddress contact = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); + when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class))) + .thenReturn(Collections.singletonList( + TestUtils.loadObjectMessage(2, "V4Pubkey.payload") + )); + final BitmessageAddress stored = new BitmessageAddress(contact.getAddress()); + stored.setAlias("Test"); + when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(stored); + + ctx.addContact(contact); + + verify(ctx.addresses(), atLeastOnce()).save(argThat(new BaseMatcher<BitmessageAddress>() { + @Override + public boolean matches(Object item) { + return item instanceof BitmessageAddress + && ((BitmessageAddress) item).getPubkey() != null + && stored.getAlias().equals(((BitmessageAddress) item).getAlias()); + } + + @Override + public void describeTo(Description description) { + description.appendText("pubkey must not be null and alias must be ").appendValue(stored.getAlias()); + } + })); + verify(ctx.internals().getProofOfWorkEngine(), never()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + } + + @Test + public void ensureSubscriptionIsAddedAndExistingBroadcastsRetrieved() throws Exception { + BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); + + List<ObjectMessage> objects = new LinkedList<>(); + objects.add(TestUtils.loadObjectMessage(4, "V4Broadcast.payload")); + objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload")); + when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class))) + .thenReturn(objects); + when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address)); + + ctx.addSubscribtion(address); + + verify(ctx.addresses(), atLeastOnce()).save(address); + assertThat(address.isSubscribed(), is(true)); + verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)); + verify(listener).receive(any(Plaintext.class)); + } + + @Test + public void ensureIdentityIsCreated() { + assertThat(ctx.createIdentity(false), notNullValue()); + } + + @Test + public void ensureMessageIsSent() throws Exception { + ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(), + "Subject", "Message"); + verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) + .putObject(object(MSG), eq(1000L), eq(1000L)); + verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Plaintext.Type.MSG)); + } + + @Test + public void ensurePubkeyIsRequestedIfItIsMissing() throws Exception { + ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), + new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + "Subject", "Message"); + verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) + .putObject(object(GET_PUBKEY), eq(1000L), eq(1000L)); + verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Plaintext.Type.MSG)); + } + + @Test(expected = IllegalArgumentException.class) + public void ensureSenderMustBeIdentity() { + ctx.send(new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + "Subject", "Message"); + } + + @Test + public void ensureBroadcastIsSent() throws Exception { + ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), + "Subject", "Message"); + verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) + .putObject(object(BROADCAST), eq(1000L), eq(1000L)); + verify(ctx.internals().getProofOfWorkEngine()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + verify(ctx.messages(), timeout(10000).atLeastOnce()) + .save(MessageMatchers.plaintext(Plaintext.Type.BROADCAST)); + } + + @Test(expected = IllegalArgumentException.class) + public void ensureSenderWithoutPrivateKeyThrowsException() { + Plaintext msg = new Plaintext.Builder(Plaintext.Type.BROADCAST) + .from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .message("Subject", "Message") + .build(); + ctx.send(msg); + } + + @Test + public void ensureChanIsJoined() { + String chanAddress = "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r"; + BitmessageAddress chan = ctx.joinChan("general", chanAddress); + assertNotNull(chan); + assertEquals(chan.getAddress(), chanAddress); + assertTrue(chan.isChan()); + } + + @Test + public void ensureDeterministicAddressesAreCreated() { + final int expected_size = 8; + List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, false); + assertEquals(expected_size, addresses.size()); + Set<String> expected = new HashSet<>(expected_size); + expected.add("BM-2cWFkyuXXFw6d393RGnin2RpSXj8wxtt6F"); + expected.add("BM-2cX8TF9vuQZEWvT7UrEeq1HN9dgiSUPLEN"); + expected.add("BM-2cUzX8f9CKUU7L8NeB8GExZvf54PrcXq1S"); + expected.add("BM-2cU7MAoQd7KE8SPF7AKFPpoEZKjk86KRqE"); + expected.add("BM-2cVm8ByVBacc2DVhdTNs6rmy5ZQK6DUsrt"); + expected.add("BM-2cW2af1vB6kWon2WkygDHqGwfcpfAFm2Jk"); + expected.add("BM-2cWdWD7UtUN4gWChgNX9pvyvNPjUZvU8BT"); + expected.add("BM-2cXkYgYcUrv4fGxSHzyEScW955Cc8sDteo"); + for (BitmessageAddress a : addresses) { + assertTrue(expected.contains(a.getAddress())); + expected.remove(a.getAddress()); + } + } + + @Test + public void ensureShortDeterministicAddressesAreCreated() { + final int expected_size = 1; + List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, true); + assertEquals(expected_size, addresses.size()); + Set<String> expected = new HashSet<>(expected_size); + expected.add("BM-NBGyBAEp6VnBkFWKpzUSgxuTqVdWPi78"); + for (BitmessageAddress a : addresses) { + assertTrue(expected.contains(a.getAddress())); + expected.remove(a.getAddress()); + } + } + + @Test + public void ensureChanIsCreated() { + BitmessageAddress chan = ctx.createChan("test"); + assertNotNull(chan); + assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION); + assertTrue(chan.isChan()); + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/DecryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/DecryptionTest.java rename to core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java diff --git a/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java b/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java new file mode 100644 index 0000000..710bf98 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.Broadcast; +import ch.dissem.bitmessage.entity.payload.GetPubkey; +import ch.dissem.bitmessage.entity.payload.Msg; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.AddressRepository; +import ch.dissem.bitmessage.ports.Labeler; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TestBase; +import ch.dissem.bitmessage.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; + +import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED; +import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static ch.dissem.bitmessage.utils.MessageMatchers.plaintext; +import static org.mockito.Mockito.*; + +/** + * @author Christian Basler + */ +public class DefaultMessageListenerTest extends TestBase { + @Mock + private AddressRepository addressRepo; + @Mock + private MessageRepository messageRepo; + + private InternalContext ctx; + private DefaultMessageListener listener; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + ctx = mock(InternalContext.class); + Singleton.initialize(new BouncyCryptography()); + when(ctx.getAddressRepository()).thenReturn(addressRepo); + when(ctx.getMessageRepository()).thenReturn(messageRepo); + + listener = new DefaultMessageListener(ctx, mock(Labeler.class), mock(BitmessageContext.Listener.class)); + } + + @Test + public void ensurePubkeyIsSentOnRequest() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + when(addressRepo.findIdentity(any(byte[].class))) + .thenReturn(identity); + listener.receive(new ObjectMessage.Builder() + .stream(2) + .payload(new GetPubkey(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))) + .build()); + verify(ctx).sendPubkey(eq(identity), eq(2L)); + } + + @Test + public void ensureIncomingPubkeyIsAddedToContact() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); + when(addressRepo.findContact(any(byte[].class))) + .thenReturn(contact); + when(messageRepo.findMessages(eq(PUBKEY_REQUESTED), eq(contact))) + .thenReturn(Collections.singletonList( + new Plaintext.Builder(MSG).from(identity).to(contact).message("S", "T").build() + )); + + ObjectMessage objectMessage = new ObjectMessage.Builder() + .stream(2) + .payload(identity.getPubkey()) + .build(); + objectMessage.sign(identity.getPrivateKey()); + objectMessage.encrypt(Singleton.security().createPublicKey(identity.getPublicDecryptionKey())); + listener.receive(objectMessage); + + verify(addressRepo).save(any(BitmessageAddress.class)); + } + + @Test + public void ensureIncomingMessageIsSaved() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); + + when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity)); + + ObjectMessage objectMessage = new ObjectMessage.Builder() + .stream(2) + .payload(new Msg(new Plaintext.Builder(MSG) + .from(identity) + .to(contact) + .message("S", "T") + .build())) + .nonce(new byte[8]) + .build(); + objectMessage.sign(identity.getPrivateKey()); + objectMessage.encrypt(identity.getPubkey()); + + listener.receive(objectMessage); + + verify(messageRepo, atLeastOnce()).save(plaintext(MSG)); + } + + @Test + public void ensureIncomingBroadcastIsSaved() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + + when(addressRepo.getSubscriptions(anyLong())).thenReturn(Collections.singletonList(identity)); + + Broadcast broadcast = Factory.getBroadcast(new Plaintext.Builder(BROADCAST) + .from(identity) + .message("S", "T") + .build()); + ObjectMessage objectMessage = new ObjectMessage.Builder() + .stream(2) + .payload(broadcast) + .nonce(new byte[8]) + .build(); + objectMessage.sign(identity.getPrivateKey()); + broadcast.encrypt(); + + listener.receive(objectMessage); + + verify(messageRepo, atLeastOnce()).save(plaintext(BROADCAST)); + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/EncryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/EncryptionTest.java rename to core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java diff --git a/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java new file mode 100644 index 0000000..e66beb1 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.Msg; +import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; + +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; + +/** + * @author Christian Basler + */ +public class ProofOfWorkServiceTest { + private ProofOfWorkService proofOfWorkService; + + private Cryptography cryptography; + @Mock + private InternalContext ctx; + @Mock + private ProofOfWorkRepository proofOfWorkRepo; + @Mock + private Inventory inventory; + @Mock + private NetworkHandler networkHandler; + @Mock + private MessageRepository messageRepo; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + cryptography = spy(new BouncyCryptography()); + Singleton.initialize(cryptography); + + ctx = mock(InternalContext.class); + when(ctx.getProofOfWorkRepository()).thenReturn(proofOfWorkRepo); + when(ctx.getInventory()).thenReturn(inventory); + when(ctx.getNetworkHandler()).thenReturn(networkHandler); + when(ctx.getMessageRepository()).thenReturn(messageRepo); + + proofOfWorkService = new ProofOfWorkService(); + proofOfWorkService.setContext(ctx); + } + + @Test + public void ensureMissingProofOfWorkIsDone() { + when(proofOfWorkRepo.getItems()).thenReturn(Arrays.asList(new byte[64])); + when(proofOfWorkRepo.getItem(any(byte[].class))).thenReturn(new ProofOfWorkRepository.Item(null, 1001, 1002)); + doNothing().when(cryptography).doProofOfWork(any(ObjectMessage.class), anyLong(), anyLong(), any(ProofOfWorkEngine.Callback.class)); + + proofOfWorkService.doMissingProofOfWork(); + + verify(cryptography).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L), + any(ProofOfWorkEngine.Callback.class)); + } + + @Test + public void ensureCalculatedNonceIsStored() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress address = TestUtils.loadContact(); + Plaintext plaintext = new Plaintext.Builder(MSG).from(identity).to(address).message("", "").build(); + ObjectMessage object = new ObjectMessage.Builder() + .payload(new Msg(plaintext)) + .build(); + object.sign(identity.getPrivateKey()); + object.encrypt(address.getPubkey()); + byte[] initialHash = new byte[64]; + byte[] nonce = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; + + when(proofOfWorkRepo.getItem(initialHash)).thenReturn(new ProofOfWorkRepository.Item(object, 1001, 1002)); + when(messageRepo.getMessage(initialHash)).thenReturn(plaintext); + + proofOfWorkService.onNonceCalculated(initialHash, nonce); + + verify(proofOfWorkRepo).removeObject(eq(initialHash)); + verify(inventory).storeObject(eq(object)); + verify(networkHandler).offer(eq(object.getInventoryVector())); + assertThat(plaintext.getInventoryVector(), equalTo(object.getInventoryVector())); + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/SignatureTest.java b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java similarity index 97% rename from domain/src/test/java/ch/dissem/bitmessage/SignatureTest.java rename to core/src/test/java/ch/dissem/bitmessage/SignatureTest.java index 71b7d2a..3566a5f 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/SignatureTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java @@ -22,7 +22,6 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.ObjectType; 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; @@ -30,7 +29,6 @@ import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; import java.io.IOException; -import java.util.Date; import static org.junit.Assert.*; diff --git a/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java similarity index 85% rename from domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java rename to core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java index e1fcc7f..9b22589 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java @@ -20,7 +20,10 @@ 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.*; +import ch.dissem.bitmessage.utils.Base58; +import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.Strings; +import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; import java.io.IOException; @@ -55,44 +58,56 @@ public class BitmessageAddressTest { } @Test - public void testCreateAddress() { + public void ensureIdentityCanBeCreated() { BitmessageAddress address = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); assertNotNull(address.getPubkey()); } @Test - public void testV2PubkeyImport() throws IOException { + public void ensureV2PubkeyCanBeImported() throws IOException { ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); Pubkey pubkey = (Pubkey) object.getPayload(); BitmessageAddress address = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - address.setPubkey(pubkey); + try { + address.setPubkey(pubkey); + } catch (Exception e) { + fail(e.getMessage()); + } } @Test - public void testV3PubkeyImport() throws IOException { + public void ensureV3PubkeyCanBeImported() throws IOException { BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe()); ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); Pubkey pubkey = (Pubkey) object.getPayload(); assertTrue(object.isSignatureValid(pubkey)); - address.setPubkey(pubkey); + try { + address.setPubkey(pubkey); + } catch (Exception e) { + fail(e.getMessage()); + } assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); } @Test - public void testV4PubkeyImport() throws IOException, DecryptionFailedException { + public void ensureV4PubkeyCanBeImported() throws IOException, DecryptionFailedException { BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload"); object.decrypt(address.getPublicDecryptionKey()); V4Pubkey pubkey = (V4Pubkey) object.getPayload(); assertTrue(object.isSignatureValid(pubkey)); - address.setPubkey(pubkey); + try { + address.setPubkey(pubkey); + } catch (Exception e) { + fail(e.getMessage()); + } } @Test - public void testV3AddressImport() throws IOException { + public void ensureV3IdentityCanBeImported() throws IOException { String address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"; assertEquals(3, new BitmessageAddress(address_string).getVersion()); assertEquals(1, new BitmessageAddress(address_string).getStream()); @@ -108,9 +123,17 @@ public class BitmessageAddressTest { } @Test - public void testGetSecret() throws IOException { - assertHexEquals("0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D", - getSecret("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ")); + public void ensureV4IdentityCanBeImported() throws IOException { + assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion()); + byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); + byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); + BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, + security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); + assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); + } + + private void assertHexEquals(String hex, byte[] bytes) { + assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase()); } private byte[] getSecret(String walletImportFormat) throws IOException { @@ -126,18 +149,4 @@ public class BitmessageAddressTest { } return Arrays.copyOfRange(bytes, 1, 33); } - - @Test - public void testV4AddressImport() throws IOException { - assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion()); - byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); - byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); - BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); - assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); - } - - private void assertHexEquals(String hex, byte[] bytes) { - assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase()); - } } diff --git a/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java similarity index 84% rename from domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java rename to core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java index 03b7bc5..5d8777a 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.*; +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; @@ -25,9 +26,11 @@ import org.junit.Test; 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 ch.dissem.bitmessage.utils.Singleton.security; import static org.junit.Assert.*; public class SerializationTest extends TestBase { @@ -95,6 +98,24 @@ public class SerializationTest extends TestBase { assertEquals(p1, p2); } + @Test + public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { + ArrayList<InventoryVector> ivs = new ArrayList<>(50000); + for (int i = 0; i < 50000; i++) { + ivs.add(new InventoryVector(security().randomBytes(32))); + } + + Inv inv = new Inv.Builder().inventory(ivs).build(); + NetworkMessage before = new NetworkMessage(inv); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + before.write(out); + + NetworkMessage after = Factory.getNetworkMessage(3, new ByteArrayInputStream(out.toByteArray())); + assertNotNull(after); + Inv invAfter = (Inv) after.getPayload(); + assertEquals(ivs, invAfter.getInventory()); + } + private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException { byte[] data = TestUtils.getBytes(resourceName); InputStream in = new ByteArrayInputStream(data); diff --git a/domain/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java b/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java similarity index 94% rename from domain/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java rename to core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java index ba5307d..1ed4aac 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java @@ -43,7 +43,7 @@ public class ProofOfWorkEngineTest extends TestBase { engine.calculateNonce(initialHash, target, new ProofOfWorkEngine.Callback() { @Override - public void onNonceCalculated(byte[] nonce) { + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { waiter1.setValue(nonce); } }); @@ -59,7 +59,7 @@ public class ProofOfWorkEngineTest extends TestBase { engine.calculateNonce(initialHash2, target2, new ProofOfWorkEngine.Callback() { @Override - public void onNonceCalculated(byte[] nonce) { + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { waiter2.setValue(nonce); } }); diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java similarity index 98% rename from domain/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java index aaba5bf..23d602c 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java @@ -57,7 +57,7 @@ public class EncodeTest { checkBytes(stream, 4, 3, 2, 1); stream = new ByteArrayOutputStream(); - Encode.int32(3355443201l, stream); + Encode.int32(3355443201L, stream); checkBytes(stream, 200, 0, 0, 1); } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java new file mode 100644 index 0000000..e423d02 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.ObjectType; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.mockito.Matchers; + +/** + * @author Christian Basler + */ +public class MessageMatchers { + public static Plaintext plaintext(final Plaintext.Type type) { + return Matchers.argThat(new BaseMatcher<Plaintext>() { + @Override + public boolean matches(Object item) { + return item instanceof Plaintext && ((Plaintext) item).getType() == type; + } + + @Override + public void describeTo(Description description) { + description.appendText("type should be ").appendValue(type); + } + }); + } + + public static ObjectMessage object(final ObjectType type) { + return Matchers.argThat(new BaseMatcher<ObjectMessage>() { + @Override + public boolean matches(Object item) { + return item instanceof ObjectMessage && ((ObjectMessage) item).getPayload().getType() == type; + } + + @Override + public void describeTo(Description description) { + description.appendText("payload type should be ").appendValue(type); + } + }); + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/TestBase.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java similarity index 74% rename from domain/src/test/java/ch/dissem/bitmessage/utils/TestBase.java rename to core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java index 1dd1335..4c73fe3 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/utils/TestBase.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java @@ -16,13 +16,15 @@ package ch.dissem.bitmessage.utils; -import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import org.junit.BeforeClass; /** - * Created by chris on 20.07.15. + * @author Christian Basler */ public class TestBase { - static { - Singleton.initialize(new BouncySecurity()); + @BeforeClass + public static void setUpClass() { + Singleton.initialize(new BouncyCryptography()); } } diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java similarity index 100% rename from domain/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java rename to core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java diff --git a/domain/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey b/core/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey similarity index 100% rename from domain/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey rename to core/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey diff --git a/domain/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey b/core/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey similarity index 100% rename from domain/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey rename to core/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey diff --git a/domain/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey b/core/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey similarity index 100% rename from domain/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey rename to core/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey diff --git a/domain/src/test/resources/V1Msg.payload b/core/src/test/resources/V1Msg.payload similarity index 100% rename from domain/src/test/resources/V1Msg.payload rename to core/src/test/resources/V1Msg.payload diff --git a/domain/src/test/resources/V1MsgStrangeData.payload b/core/src/test/resources/V1MsgStrangeData.payload similarity index 100% rename from domain/src/test/resources/V1MsgStrangeData.payload rename to core/src/test/resources/V1MsgStrangeData.payload diff --git a/domain/src/test/resources/V2GetPubkey.payload b/core/src/test/resources/V2GetPubkey.payload similarity index 100% rename from domain/src/test/resources/V2GetPubkey.payload rename to core/src/test/resources/V2GetPubkey.payload diff --git a/domain/src/test/resources/V2Pubkey.payload b/core/src/test/resources/V2Pubkey.payload similarity index 100% rename from domain/src/test/resources/V2Pubkey.payload rename to core/src/test/resources/V2Pubkey.payload diff --git a/domain/src/test/resources/V3GetPubkey.payload b/core/src/test/resources/V3GetPubkey.payload similarity index 100% rename from domain/src/test/resources/V3GetPubkey.payload rename to core/src/test/resources/V3GetPubkey.payload diff --git a/domain/src/test/resources/V3Pubkey.payload b/core/src/test/resources/V3Pubkey.payload similarity index 100% rename from domain/src/test/resources/V3Pubkey.payload rename to core/src/test/resources/V3Pubkey.payload diff --git a/domain/src/test/resources/V4Broadcast.payload b/core/src/test/resources/V4Broadcast.payload similarity index 100% rename from domain/src/test/resources/V4Broadcast.payload rename to core/src/test/resources/V4Broadcast.payload diff --git a/domain/src/test/resources/V4GetPubkey.payload b/core/src/test/resources/V4GetPubkey.payload similarity index 100% rename from domain/src/test/resources/V4GetPubkey.payload rename to core/src/test/resources/V4GetPubkey.payload diff --git a/domain/src/test/resources/V4Pubkey.payload b/core/src/test/resources/V4Pubkey.payload similarity index 100% rename from domain/src/test/resources/V4Pubkey.payload rename to core/src/test/resources/V4Pubkey.payload diff --git a/domain/src/test/resources/V5Broadcast.payload b/core/src/test/resources/V5Broadcast.payload similarity index 100% rename from domain/src/test/resources/V5Broadcast.payload rename to core/src/test/resources/V5Broadcast.payload diff --git a/security-bc/build.gradle b/cryptography-bc/build.gradle similarity index 55% rename from security-bc/build.gradle rename to cryptography-bc/build.gradle index ff37994..c09b0db 100644 --- a/security-bc/build.gradle +++ b/cryptography-bc/build.gradle @@ -2,16 +2,16 @@ uploadArchives { repositories { mavenDeployer { pom.project { - name 'Jabit Bouncy Security' - artifactId = 'jabit-security-bouncy' - description 'The Security implementation using bouncy castle' + name 'Jabit Bouncy Cryptography' + artifactId = 'jabit-cryptography-bouncy' + description 'The Cryptography implementation using bouncy castle' } } } } dependencies { - compile project(':domain') + compile project(':core') compile 'org.bouncycastle:bcprov-jdk15on:1.52' testCompile 'junit:junit:4.11' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/security-bc/src/main/java/ch/dissem/bitmessage/security/bc/BouncySecurity.java b/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java similarity index 82% rename from security-bc/src/main/java/ch/dissem/bitmessage/security/bc/BouncySecurity.java rename to cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java index a125049..377135c 100644 --- a/security-bc/src/main/java/ch/dissem/bitmessage/security/bc/BouncySecurity.java +++ b/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package ch.dissem.bitmessage.security.bc; +package ch.dissem.bitmessage.cryptography.bc; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.ports.AbstractSecurity; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.ports.AbstractCryptography; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.BufferedBlockCipher; import org.bouncycastle.crypto.CipherParameters; @@ -37,6 +38,7 @@ import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; +import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; @@ -47,21 +49,25 @@ import java.util.Arrays; * As Spongycastle can't be used on the Oracle JVM, and Bouncycastle doesn't work properly on Android (thanks, Google), * this is the Bouncycastle implementation. */ -public class BouncySecurity extends AbstractSecurity { +public class BouncyCryptography extends AbstractCryptography { private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); + private static final String ALGORITHM_ECDSA = "ECDSA"; + private static final String PROVIDER = "BC"; static { java.security.Security.addProvider(new BouncyCastleProvider()); } - public BouncySecurity() { - super("BC"); + public BouncyCryptography() { + super(PROVIDER); } @Override public byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector) { - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); - + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESEngine()), + new PKCS7Padding() + ); CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); cipher.init(encrypt, params); @@ -103,14 +109,14 @@ public class BouncySecurity extends AbstractSecurity { ECPoint Q = keyToPoint(pubkey.getSigningKey()); KeySpec keySpec = new ECPublicKeySpec(Q, spec); - PublicKey publicKey = KeyFactory.getInstance("ECDSA", "BC").generatePublic(keySpec); + PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER).generatePublic(keySpec); - Signature sig = Signature.getInstance("ECDSA", "BC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initVerify(publicKey); sig.update(data); return sig.verify(signature); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } @@ -127,14 +133,15 @@ public class BouncySecurity extends AbstractSecurity { BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); KeySpec keySpec = new ECPrivateKeySpec(d, spec); - java.security.PrivateKey privKey = KeyFactory.getInstance("ECDSA", "BC").generatePrivate(keySpec); + java.security.PrivateKey privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER) + .generatePrivate(keySpec); - Signature sig = Signature.getInstance("ECDSA", "BC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initSign(privKey); sig.update(data); return sig.sign(); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } diff --git a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java new file mode 100644 index 0000000..3e5695c --- /dev/null +++ b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java @@ -0,0 +1,159 @@ +package ch.dissem.bitmessage.security; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.GenericPayload; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; +import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; +import ch.dissem.bitmessage.utils.CallbackWaiter; +import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.UnixTime; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.xml.bind.DatatypeConverter; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static ch.dissem.bitmessage.utils.UnixTime.DAY; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Christian Basler + */ +public class CryptographyTest { + public static final byte[] TEST_VALUE = "teststring".getBytes(); + public static final byte[] TEST_SHA1 = DatatypeConverter.parseHexBinary("" + + "b8473b86d4c2072ca9b08bd28e373e8253e865c4"); + public static final byte[] TEST_SHA512 = DatatypeConverter.parseHexBinary("" + + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" + + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72"); + public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" + + "cd566972b5e50104011a92b59fa8e0b1234851ae"); + + private static BouncyCryptography crypto; + + @BeforeClass + public static void setUp() { + crypto = new BouncyCryptography(); + Singleton.initialize(crypto); + InternalContext ctx = mock(InternalContext.class); + when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); + crypto.setContext(ctx); + } + + @Test + public void testRipemd160() { + assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE)); + } + + @Test + public void testSha1() { + assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE)); + } + + @Test + public void testSha512() { + assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE)); + } + + @Test + public void testChaining() { + assertArrayEquals(TEST_SHA512, crypto.sha512("test".getBytes(), "string".getBytes())); + } + + @Test + public void ensureDoubleHashYieldsSameResultAsHashOfHash() { + assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)); + } + + @Test(expected = IOException.class) + public void ensureExceptionForInsufficientProofOfWork() throws IOException { + ObjectMessage objectMessage = new ObjectMessage.Builder() + .nonce(new byte[8]) + .expiresTime(UnixTime.now(+28 * DAY)) + .objectType(0) + .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) + .build(); + crypto.checkProofOfWork(objectMessage, 1000, 1000); + } + + @Test + public void testDoProofOfWork() throws Exception { + ObjectMessage objectMessage = new ObjectMessage.Builder() + .nonce(new byte[8]) + .expiresTime(UnixTime.now(+2 * MINUTE)) + .objectType(0) + .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) + .build(); + final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); + crypto.doProofOfWork(objectMessage, 1000, 1000, + new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + waiter.setValue(nonce); + } + }); + objectMessage.setNonce(waiter.waitForValue()); + try { + crypto.checkProofOfWork(objectMessage, 1000, 1000); + } catch (InsufficientProofOfWorkException e) { + fail(e.getMessage()); + } + } + + @Test + public void ensureEncryptionAndDecryptionWorks() { + byte[] data = crypto.randomBytes(100); + byte[] key_e = crypto.randomBytes(32); + byte[] iv = crypto.randomBytes(16); + byte[] encrypted = crypto.crypt(true, data, key_e, iv); + byte[] decrypted = crypto.crypt(false, encrypted, key_e, iv); + assertArrayEquals(data, decrypted); + } + + @Test(expected = IllegalArgumentException.class) + public void ensureDecryptionFailsWithInvalidCypherText() { + byte[] data = crypto.randomBytes(128); + byte[] key_e = crypto.randomBytes(32); + byte[] iv = crypto.randomBytes(16); + crypto.crypt(false, data, key_e, iv); + } + + @Test + public void testMultiplication() { + byte[] a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); + byte[] A = crypto.createPublicKey(a); + + byte[] b = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); + byte[] B = crypto.createPublicKey(b); + + assertArrayEquals(crypto.multiply(A, b), crypto.multiply(B, a)); + } + + @Test + public void ensureSignatureIsValid() { + byte[] data = crypto.randomBytes(100); + PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); + byte[] signature = crypto.getSignature(data, privateKey); + assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(true)); + } + + @Test + public void ensureSignatureIsInvalidForTemperedData() { + byte[] data = crypto.randomBytes(100); + PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); + byte[] signature = crypto.getSignature(data, privateKey); + data[0]++; + assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(false)); + } +} diff --git a/cryptography-sc/build.gradle b/cryptography-sc/build.gradle new file mode 100644 index 0000000..16771fc --- /dev/null +++ b/cryptography-sc/build.gradle @@ -0,0 +1,17 @@ +uploadArchives { + repositories { + mavenDeployer { + pom.project { + name 'Jabit Spongy Cryptography' + artifactId = 'jabit-cryptography-spongy' + description 'The Cryptography implementation using spongy castle (needed for Android)' + } + } + } +} + +dependencies { + compile project(':core') + compile 'com.madgag.spongycastle:prov:1.52.0.0' + testCompile 'junit:junit:4.11' +} diff --git a/security-sc/src/main/java/ch/dissem/bitmessage/security/sc/SpongySecurity.java b/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java similarity index 82% rename from security-sc/src/main/java/ch/dissem/bitmessage/security/sc/SpongySecurity.java rename to cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java index 70a0743..b90e1c8 100644 --- a/security-sc/src/main/java/ch/dissem/bitmessage/security/sc/SpongySecurity.java +++ b/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package ch.dissem.bitmessage.security.sc; +package ch.dissem.bitmessage.cryptography.sc; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.ports.AbstractSecurity; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.ports.AbstractCryptography; import org.spongycastle.asn1.x9.X9ECParameters; import org.spongycastle.crypto.BufferedBlockCipher; import org.spongycastle.crypto.CipherParameters; @@ -37,6 +38,7 @@ import org.spongycastle.jce.spec.ECPublicKeySpec; import org.spongycastle.math.ec.ECPoint; import java.math.BigInteger; +import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; @@ -47,21 +49,25 @@ import java.util.Arrays; * As Spongycastle can't be used on the Oracle JVM, and Bouncycastle doesn't work properly on Android (thanks, Google), * this is the Spongycastle implementation. */ -public class SpongySecurity extends AbstractSecurity { +public class SpongyCryptography extends AbstractCryptography { private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); + private static final String ALGORITHM_ECDSA = "ECDSA"; + private static final String PROVIDER = "SC"; static { java.security.Security.addProvider(new BouncyCastleProvider()); } - public SpongySecurity() { - super("SC"); + public SpongyCryptography() { + super(PROVIDER); } @Override public byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector) { - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); - + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESEngine()), + new PKCS7Padding() + ); CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); cipher.init(encrypt, params); @@ -103,14 +109,14 @@ public class SpongySecurity extends AbstractSecurity { ECPoint Q = keyToPoint(pubkey.getSigningKey()); KeySpec keySpec = new ECPublicKeySpec(Q, spec); - PublicKey publicKey = KeyFactory.getInstance("ECDSA", "SC").generatePublic(keySpec); + PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER).generatePublic(keySpec); - Signature sig = Signature.getInstance("ECDSA", "SC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initVerify(publicKey); sig.update(data); return sig.verify(signature); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } @@ -127,14 +133,15 @@ public class SpongySecurity extends AbstractSecurity { BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); KeySpec keySpec = new ECPrivateKeySpec(d, spec); - java.security.PrivateKey privKey = KeyFactory.getInstance("ECDSA", "SC").generatePrivate(keySpec); + java.security.PrivateKey privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER) + .generatePrivate(keySpec); - Signature sig = Signature.getInstance("ECDSA", "SC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initSign(privKey); sig.update(data); return sig.sign(); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } diff --git a/demo/build.gradle b/demo/build.gradle index e4c51a7..d87b0f3 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -16,18 +16,21 @@ uploadArchives { sourceCompatibility = 1.8 +test.enabled = Boolean.valueOf(systemTestsEnabled) + task fatCapsule(type: FatCapsule) { applicationClass 'ch.dissem.bitmessage.demo.Main' } dependencies { - compile project(':domain') + compile project(':core') compile project(':networking') compile project(':repositories') - compile project(':security-bc') + compile project(':cryptography-bc') compile project(':wif') compile 'org.slf4j:slf4j-simple:1.7.12' compile 'args4j:args4j:2.32' compile 'com.h2database:h2:1.4.190' testCompile 'junit:junit:4.11' + testCompile 'org.mockito:mockito-core:1.10.19' } diff --git a/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java b/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java index a065de9..f3d9579 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java @@ -17,57 +17,59 @@ package ch.dissem.bitmessage.demo; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; -import ch.dissem.bitmessage.repository.JdbcAddressRepository; -import ch.dissem.bitmessage.repository.JdbcConfig; -import ch.dissem.bitmessage.repository.JdbcInventory; -import ch.dissem.bitmessage.repository.JdbcMessageRepository; -import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.repository.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; import java.util.List; -import java.util.Scanner; +import java.util.stream.Collectors; + +import static ch.dissem.bitmessage.demo.CommandLine.COMMAND_BACK; +import static ch.dissem.bitmessage.demo.CommandLine.ERROR_UNKNOWN_COMMAND; /** * A simple command line Bitmessage application */ public class Application { private final static Logger LOG = LoggerFactory.getLogger(Application.class); - private final Scanner scanner; + private final CommandLine commandLine; private BitmessageContext ctx; - public Application() { + public Application(InetAddress syncServer, int syncPort) { JdbcConfig jdbcConfig = new JdbcConfig(); ctx = new BitmessageContext.Builder() .addressRepo(new JdbcAddressRepository(jdbcConfig)) .inventory(new JdbcInventory(jdbcConfig)) .nodeRegistry(new MemoryNodeRegistry()) .messageRepo(new JdbcMessageRepository(jdbcConfig)) + .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) .networkHandler(new DefaultNetworkHandler()) - .security(new BouncySecurity()) + .cryptography(new BouncyCryptography()) .port(48444) - .listener(new BitmessageContext.Listener() { - @Override - public void receive(Plaintext plaintext) { - try { - System.out.println(new String(plaintext.getMessage(), "UTF-8")); - } catch (UnsupportedEncodingException e) { - LOG.error(e.getMessage(), e); - } + .listener(plaintext -> { + try { + System.out.println(new String(plaintext.getMessage(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + LOG.error(e.getMessage(), e); } }) .build(); - ctx.startup(); + if (syncServer == null) { + ctx.startup(); + } - scanner = new Scanner(System.in); + commandLine = new CommandLine(); String command; do { @@ -77,10 +79,13 @@ public class Application { System.out.println("c) contacts"); System.out.println("s) subscriptions"); System.out.println("m) messages"); + if (syncServer != null) { + System.out.println("y) sync"); + } System.out.println("?) info"); System.out.println("e) exit"); - command = nextCommand(); + command = commandLine.nextCommand(); try { switch (command) { case "i": { @@ -101,8 +106,13 @@ public class Application { break; case "e": break; + case "y": + if (syncServer != null) { + ctx.synchronize(syncServer, syncPort, 120, true); + } + break; default: - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } catch (Exception e) { LOG.debug(e.getMessage()); @@ -118,37 +128,26 @@ public class Application { System.out.println(ctx.status()); } - private String nextCommand() { - return scanner.nextLine().trim().toLowerCase(); - } - private void identities() { String command; List<BitmessageAddress> identities = ctx.addresses().getIdentities(); do { System.out.println(); - int i = 0; - for (BitmessageAddress identity : identities) { - i++; - System.out.print(i + ") "); - if (identity.getAlias() != null) { - System.out.println(identity.getAlias() + " (" + identity.getAddress() + ")"); - } else { - System.out.println(identity.getAddress()); - } - } - if (i == 0) { - System.out.println("You have no identities yet."); - } + commandLine.listAddresses(identities, "identities"); System.out.println("a) create identity"); - System.out.println("b) back"); + System.out.println("c) join chan"); + System.out.println(COMMAND_BACK); - command = nextCommand(); + command = commandLine.nextCommand(); switch (command) { case "a": addIdentity(); identities = ctx.addresses().getIdentities(); break; + case "c": + joinChan(); + identities = ctx.addresses().getIdentities(); + break; case "b": return; default: @@ -156,7 +155,7 @@ public class Application { int index = Integer.parseInt(command) - 1; address(identities.get(index)); } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equals(command)); @@ -164,38 +163,35 @@ public class Application { private void addIdentity() { System.out.println(); - BitmessageAddress identity = ctx.createIdentity(yesNo("would you like a shorter address? This will take some time to calculate."), Pubkey.Feature.DOES_ACK); + BitmessageAddress identity = ctx.createIdentity(commandLine.yesNo("would you like a shorter address? This will take some time to calculate."), Pubkey.Feature.DOES_ACK); System.out.println("Please enter an alias for this identity, or an empty string for none"); - String alias = scanner.nextLine().trim(); + String alias = commandLine.nextLineTrimmed(); if (alias.length() > 0) { identity.setAlias(alias); } ctx.addresses().save(identity); } + private void joinChan() { + System.out.println(); + System.out.print("Passphrase: "); + String passphrase = commandLine.nextLine(); + System.out.print("Address: "); + String address = commandLine.nextLineTrimmed(); + ctx.joinChan(passphrase, address); + } + private void contacts() { String command; List<BitmessageAddress> contacts = ctx.addresses().getContacts(); do { System.out.println(); - int i = 0; - for (BitmessageAddress contact : contacts) { - i++; - System.out.print(i + ") "); - if (contact.getAlias() != null) { - System.out.println(contact.getAlias() + " (" + contact.getAddress() + ")"); - } else { - System.out.println(contact.getAddress()); - } - } - if (i == 0) { - System.out.println("You have no contacts yet."); - } + commandLine.listAddresses(contacts, "contacts"); System.out.println(); System.out.println("a) add contact"); - System.out.println("b) back"); + System.out.println(COMMAND_BACK); - command = nextCommand(); + command = commandLine.nextCommand(); switch (command) { case "a": addContact(false); @@ -208,7 +204,7 @@ public class Application { int index = Integer.parseInt(command) - 1; address(contacts.get(index)); } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equals(command)); @@ -218,9 +214,9 @@ public class Application { System.out.println(); System.out.println("Please enter the Bitmessage address you want to add"); try { - BitmessageAddress address = new BitmessageAddress(scanner.nextLine().trim()); + BitmessageAddress address = new BitmessageAddress(commandLine.nextLineTrimmed()); System.out.println("Please enter an alias for this address, or an empty string for none"); - String alias = scanner.nextLine().trim(); + String alias = commandLine.nextLineTrimmed(); if (alias.length() > 0) { address.setAlias(alias); } @@ -238,24 +234,12 @@ public class Application { List<BitmessageAddress> subscriptions = ctx.addresses().getSubscriptions(); do { System.out.println(); - int i = 0; - for (BitmessageAddress contact : subscriptions) { - i++; - System.out.print(i + ") "); - if (contact.getAlias() != null) { - System.out.println(contact.getAlias() + " (" + contact.getAddress() + ")"); - } else { - System.out.println(contact.getAddress()); - } - } - if (i == 0) { - System.out.println("You have no subscriptions yet."); - } + commandLine.listAddresses(subscriptions, "subscriptions"); System.out.println(); System.out.println("a) add subscription"); - System.out.println("b) back"); + System.out.println(COMMAND_BACK); - command = nextCommand(); + command = commandLine.nextCommand(); switch (command) { case "a": addContact(true); @@ -268,7 +252,7 @@ public class Application { int index = Integer.parseInt(command) - 1; address(subscriptions.get(index)); } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equals(command)); @@ -282,18 +266,24 @@ public class Application { System.out.println("Stream: " + address.getStream()); System.out.println("Version: " + address.getVersion()); if (address.getPrivateKey() == null) { - if (address.getPubkey() != null) { - System.out.println("Public key available"); - } else { + if (address.getPubkey() == null) { System.out.println("Public key still missing"); + } else { + System.out.println("Public key available"); + } + } else { + if (address.isChan()) { + System.out.println("Chan"); + } else { + System.out.println("Identity"); } } } private void messages() { String command; - List<Plaintext> messages = ctx.messages().findMessages(Plaintext.Status.RECEIVED); do { + List<Plaintext> messages = ctx.messages().findMessages(Plaintext.Status.RECEIVED); System.out.println(); int i = 0; for (Plaintext message : messages) { @@ -306,9 +296,9 @@ public class Application { System.out.println(); System.out.println("c) compose message"); System.out.println("s) compose broadcast"); - System.out.println("b) back"); + System.out.println(COMMAND_BACK); - command = scanner.nextLine().trim(); + command = commandLine.nextCommand(); switch (command) { case "c": compose(false); @@ -323,7 +313,7 @@ public class Application { int index = Integer.parseInt(command) - 1; show(messages.get(index)); } catch (NumberFormatException | IndexOutOfBoundsException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equalsIgnoreCase(command)); @@ -337,14 +327,16 @@ public class Application { System.out.println(); System.out.println(message.getText()); System.out.println(); - System.out.println("Labels: " + message.getLabels()); + System.out.println(message.getLabels().stream().map(Label::toString).collect( + Collectors.joining("Labels: ", ", ", ""))); System.out.println(); + ctx.labeler().markAsRead(message); String command; do { System.out.println("r) reply"); System.out.println("d) delete"); - System.out.println("b) back"); - command = nextCommand(); + System.out.println(COMMAND_BACK); + command = commandLine.nextCommand(); switch (command) { case "r": compose(message.getTo(), message.getFrom(), "RE: " + message.getSubject()); @@ -354,18 +346,18 @@ public class Application { case "b": return; default: - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } while (!"b".equalsIgnoreCase(command)); } private void compose(boolean broadcast) { System.out.println(); - BitmessageAddress from = selectAddress(true); + BitmessageAddress from = selectIdentity(); if (from == null) { return; } - BitmessageAddress to = (broadcast ? null : selectAddress(false)); + BitmessageAddress to = (broadcast ? null : selectContact()); if (!broadcast && to == null) { return; } @@ -373,58 +365,22 @@ public class Application { compose(from, to, null); } - private BitmessageAddress selectAddress(boolean id) { - List<BitmessageAddress> addresses = (id ? ctx.addresses().getIdentities() : ctx.addresses().getContacts()); + private BitmessageAddress selectIdentity() { + List<BitmessageAddress> addresses = ctx.addresses().getIdentities(); while (addresses.size() == 0) { - if (id) { - addIdentity(); - addresses = ctx.addresses().getIdentities(); - } else { - addContact(false); - addresses = ctx.addresses().getContacts(); - } + addIdentity(); + addresses = ctx.addresses().getIdentities(); } - if (addresses.size() == 1) { - return addresses.get(0); + return commandLine.selectAddress(addresses, "From:"); + } + + private BitmessageAddress selectContact() { + List<BitmessageAddress> addresses = ctx.addresses().getContacts(); + while (addresses.size() == 0) { + addContact(false); + addresses = ctx.addresses().getContacts(); } - - String command; - do { - System.out.println(); - if (id) { - System.out.println("From:"); - } else { - System.out.println("To:"); - } - - int i = 0; - for (BitmessageAddress identity : addresses) { - i++; - System.out.print(i + ") "); - if (identity.getAlias() != null) { - System.out.println(identity.getAlias() + " (" + identity.getAddress() + ")"); - } else { - System.out.println(identity.getAddress()); - } - } - System.out.println("b) back"); - - command = nextCommand(); - switch (command) { - case "b": - return null; - default: - try { - int index = Integer.parseInt(command) - 1; - if (addresses.get(index) != null) { - return addresses.get(index); - } - } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); - } - } - } while (!"b".equals(command)); - return null; + return commandLine.selectAddress(addresses, "To:"); } private void compose(BitmessageAddress from, BitmessageAddress to, String subject) { @@ -438,29 +394,19 @@ public class Application { System.out.println("Subject: " + subject); } else { System.out.print("Subject: "); - subject = scanner.nextLine().trim(); + subject = commandLine.nextLineTrimmed(); } System.out.println("Message:"); StringBuilder message = new StringBuilder(); String line; do { - line = scanner.nextLine(); + line = commandLine.nextLine(); message.append(line).append('\n'); - } while (line.length() > 0 || !yesNo("Send message?")); + } while (line.length() > 0 || !commandLine.yesNo("Send message?")); if (broadcast) { ctx.broadcast(from, subject, message.toString()); } else { ctx.send(from, to, subject, message.toString()); } } - - private boolean yesNo(String question) { - String answer; - do { - System.out.println(question + " (y/n)"); - answer = scanner.nextLine(); - if ("y".equalsIgnoreCase(answer)) return true; - if ("n".equalsIgnoreCase(answer)) return false; - } while (true); - } } diff --git a/demo/src/main/java/ch/dissem/bitmessage/demo/CommandLine.java b/demo/src/main/java/ch/dissem/bitmessage/demo/CommandLine.java new file mode 100644 index 0000000..f04196a --- /dev/null +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/CommandLine.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.demo; + +import ch.dissem.bitmessage.entity.BitmessageAddress; + +import java.util.List; +import java.util.Scanner; + + +/** + * @author Christian Basler + */ +public class CommandLine { + public static final String COMMAND_BACK = "b) back"; + public static final String ERROR_UNKNOWN_COMMAND = "Unknown command. Please try again."; + + private Scanner scanner = new Scanner(System.in); + + public String nextCommand() { + return scanner.nextLine().trim().toLowerCase(); + } + + public String nextLine() { + return scanner.nextLine(); + } + + public String nextLineTrimmed() { + return scanner.nextLine(); + } + + public boolean yesNo(String question) { + String answer; + do { + System.out.println(question + " (y/n)"); + answer = scanner.nextLine(); + if ("y".equalsIgnoreCase(answer)) return true; + if ("n".equalsIgnoreCase(answer)) return false; + } while (true); + } + + public BitmessageAddress selectAddress(List<BitmessageAddress> addresses, String label) { + if (addresses.size() == 1) { + return addresses.get(0); + } + + String command; + do { + System.out.println(); + System.out.println(label); + + listAddresses(addresses, "contacts"); + System.out.println(COMMAND_BACK); + + command = nextCommand(); + switch (command) { + case "b": + return null; + default: + try { + int index = Integer.parseInt(command) - 1; + if (addresses.get(index) != null) { + return addresses.get(index); + } + } catch (NumberFormatException e) { + System.out.println(ERROR_UNKNOWN_COMMAND); + } + } + } while (!"b".equals(command)); + return null; + } + + public void listAddresses(List<BitmessageAddress> addresses, String kind) { + int i = 0; + for (BitmessageAddress address : addresses) { + i++; + System.out.print(i + ") "); + if (address.getAlias() == null) { + System.out.println(address.getAddress()); + } else { + System.out.println(address.getAlias() + " (" + address.getAddress() + ")"); + } + } + if (i == 0) { + System.out.println("You have no " + kind + " yet."); + } + } +} diff --git a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java index ac90e88..2532796 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java @@ -17,10 +17,10 @@ package ch.dissem.bitmessage.demo; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.repository.*; -import ch.dissem.bitmessage.security.bc.BouncySecurity; import ch.dissem.bitmessage.wif.WifExporter; import ch.dissem.bitmessage.wif.WifImporter; import org.kohsuke.args4j.CmdLineException; @@ -29,6 +29,7 @@ import org.kohsuke.args4j.Option; import java.io.File; import java.io.IOException; +import java.net.InetAddress; public class Main { public static void main(String[] args) throws IOException { @@ -51,8 +52,9 @@ public class Main { .inventory(new JdbcInventory(jdbcConfig)) .nodeRegistry(new MemoryNodeRegistry()) .messageRepo(new JdbcMessageRepository(jdbcConfig)) + .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) .networkHandler(new DefaultNetworkHandler()) - .security(new BouncySecurity()) + .cryptography(new BouncyCryptography()) .port(48444) .build(); @@ -63,7 +65,8 @@ public class Main { new WifImporter(ctx, options.importWIF).importAll(); } } else { - new Application(); + InetAddress syncServer = options.syncServer == null ? null : InetAddress.getByName(options.syncServer); + new Application(syncServer, options.syncPort); } } @@ -73,5 +76,11 @@ public class Main { @Option(name = "-export", usage = "Export to WIF file.") private File exportWIF; + + @Option(name = "-syncServer", usage = "Use manual synchronization with the given server instead of starting a full node.") + private String syncServer; + + @Option(name = "-syncPort", usage = "Port to use for synchronisation") + private int syncPort = 8444; } } diff --git a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java new file mode 100644 index 0000000..06b6628 --- /dev/null +++ b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java @@ -0,0 +1,98 @@ +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.repository.*; +import ch.dissem.bitmessage.utils.TTL; +import org.junit.*; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Basler + */ +public class SystemTest { + private static int port = 6000; + + private BitmessageContext alice; + private TestListener aliceListener = new TestListener(); + private BitmessageAddress aliceIdentity; + + private BitmessageContext bob; + private TestListener bobListener = new TestListener(); + private BitmessageAddress bobIdentity; + + @Before + public void setUp() { + int alicePort = port++; + int bobPort = port++; + TTL.msg(5 * MINUTE); + TTL.getpubkey(5 * MINUTE); + TTL.pubkey(5 * MINUTE); + JdbcConfig aliceDB = new JdbcConfig("jdbc:h2:mem:alice;DB_CLOSE_DELAY=-1", "sa", ""); + alice = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(aliceDB)) + .inventory(new JdbcInventory(aliceDB)) + .messageRepo(new JdbcMessageRepository(aliceDB)) + .powRepo(new JdbcProofOfWorkRepository(aliceDB)) + .port(alicePort) + .nodeRegistry(new TestNodeRegistry(bobPort)) + .networkHandler(new DefaultNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(aliceListener) + .build(); + alice.startup(); + aliceIdentity = alice.createIdentity(false); + + JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); + bob = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(bobDB)) + .inventory(new JdbcInventory(bobDB)) + .messageRepo(new JdbcMessageRepository(bobDB)) + .powRepo(new JdbcProofOfWorkRepository(bobDB)) + .port(bobPort) + .nodeRegistry(new TestNodeRegistry(alicePort)) + .networkHandler(new DefaultNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(bobListener) + .build(); + bob.startup(); + bobIdentity = bob.createIdentity(false); + } + + @After + public void tearDown() { + alice.shutdown(); + bob.shutdown(); + } + + @Test + public void ensureAliceCanSendMessageToBob() throws Exception { + String originalMessage = UUID.randomUUID().toString(); + alice.send(aliceIdentity, new BitmessageAddress(bobIdentity.getAddress()), "Subject", originalMessage); + + Plaintext plaintext = bobListener.get(15, TimeUnit.MINUTES); + + assertThat(plaintext.getType(), equalTo(Plaintext.Type.MSG)); + assertThat(plaintext.getText(), equalTo(originalMessage)); + } + + @Test + public void ensureBobCanReceiveBroadcastFromAlice() throws Exception { + String originalMessage = UUID.randomUUID().toString(); + bob.addSubscribtion(new BitmessageAddress(aliceIdentity.getAddress())); + alice.broadcast(aliceIdentity, "Subject", originalMessage); + + Plaintext plaintext = bobListener.get(15, TimeUnit.MINUTES); + + assertThat(plaintext.getType(), equalTo(Plaintext.Type.BROADCAST)); + assertThat(plaintext.getText(), equalTo(originalMessage)); + } +} diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestListener.java b/demo/src/test/java/ch/dissem/bitmessage/TestListener.java new file mode 100644 index 0000000..9c00776 --- /dev/null +++ b/demo/src/test/java/ch/dissem/bitmessage/TestListener.java @@ -0,0 +1,26 @@ +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.Plaintext; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * Created by chrig on 02.02.2016. + */ +public class TestListener implements BitmessageContext.Listener { + private CompletableFuture<Plaintext> future = new CompletableFuture<>(); + + @Override + public void receive(Plaintext plaintext) { + future.complete(plaintext); + } + + public void reset() { + future = new CompletableFuture<>(); + } + + public Plaintext get(long timeout, TimeUnit unit) throws Exception { + return future.get(timeout, unit); + } +} diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java b/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java new file mode 100644 index 0000000..71750ed --- /dev/null +++ b/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java @@ -0,0 +1,51 @@ +/* + * 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.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.NodeRegistry; + +import java.util.LinkedList; +import java.util.List; + +/** + * Empty {@link NodeRegistry} that doesn't do anything, but shouldn't break things either. + */ +class TestNodeRegistry implements NodeRegistry { + private List<NetworkAddress> nodes = new LinkedList<>(); + + public TestNodeRegistry(int... ports) { + for (int port : ports) { + nodes.add( + new NetworkAddress.Builder() + .ipv4(127, 0, 0, 1) + .port(port) + .build() + ); + } + } + + @Override + public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { + return nodes; + } + + @Override + public void offerAddresses(List<NetworkAddress> addresses) { + // Ignore + } +} diff --git a/extensions/build.gradle b/extensions/build.gradle new file mode 100644 index 0000000..d44f900 --- /dev/null +++ b/extensions/build.gradle @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +uploadArchives { + repositories { + mavenDeployer { + pom.project { + name 'Jabit Extensions' + artifactId = 'jabit-extensions' + description 'Protocol extensions used for some extended features, e.g. server and mobile client.' + } + } + } +} + +dependencies { + compile project(':core') + testCompile 'junit:junit:4.11' + testCompile 'org.slf4j:slf4j-simple:1.7.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile project(path: ':core', configuration: 'testArtifacts') + testCompile project(':cryptography-bc') +} diff --git a/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java b/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java new file mode 100644 index 0000000..0b5b142 --- /dev/null +++ b/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.extensions; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.entity.payload.CryptoBox; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.*; + +import static ch.dissem.bitmessage.utils.Decode.*; +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * A {@link CustomMessage} implementation that contains signed and encrypted data. + * + * @author Christian Basler + */ +public class CryptoCustomMessage<T extends Streamable> extends CustomMessage { + private static final long serialVersionUID = 7395193565986284426L; + + public static final String COMMAND = "ENCRYPTED"; + + private final Reader<T> dataReader; + private CryptoBox container; + private BitmessageAddress sender; + private T data; + + public CryptoCustomMessage(T data) throws IOException { + super(COMMAND); + this.data = data; + this.dataReader = null; + } + + private CryptoCustomMessage(CryptoBox container, Reader<T> dataReader) { + super(COMMAND); + this.container = container; + this.dataReader = dataReader; + } + + public static <T extends Streamable> CryptoCustomMessage<T> read(CustomMessage data, Reader<T> dataReader) throws IOException { + CryptoBox cryptoBox = CryptoBox.read(new ByteArrayInputStream(data.getData()), data.getData().length); + return new CryptoCustomMessage<>(cryptoBox, dataReader); + } + + public BitmessageAddress getSender() { + return sender; + } + + public void signAndEncrypt(BitmessageAddress identity, byte[] publicKey) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + Encode.varInt(identity.getVersion(), out); + Encode.varInt(identity.getStream(), out); + Encode.int32(identity.getPubkey().getBehaviorBitfield(), out); + out.write(identity.getPubkey().getSigningKey(), 1, 64); + out.write(identity.getPubkey().getEncryptionKey(), 1, 64); + if (identity.getVersion() >= 3) { + Encode.varInt(identity.getPubkey().getNonceTrialsPerByte(), out); + Encode.varInt(identity.getPubkey().getExtraBytes(), out); + } + + data.write(out); + Encode.varBytes(security().getSignature(out.toByteArray(), identity.getPrivateKey()), out); + container = new CryptoBox(out.toByteArray(), publicKey); + } + + public T decrypt(byte[] privateKey) throws IOException, DecryptionFailedException { + SignatureCheckingInputStream in = new SignatureCheckingInputStream(container.decrypt(privateKey)); + + long addressVersion = varInt(in); + long stream = varInt(in); + int behaviorBitfield = int32(in); + byte[] publicSigningKey = bytes(in, 64); + byte[] publicEncryptionKey = bytes(in, 64); + long nonceTrialsPerByte = addressVersion >= 3 ? varInt(in) : 0; + long extraBytes = addressVersion >= 3 ? varInt(in) : 0; + + sender = new BitmessageAddress(Factory.createPubkey( + addressVersion, + stream, + publicSigningKey, + publicEncryptionKey, + nonceTrialsPerByte, + extraBytes, + behaviorBitfield + )); + + data = dataReader.read(sender, in); + + in.checkSignature(sender.getPubkey()); + + return data; + } + + @Override + public void write(OutputStream out) throws IOException { + Encode.varString(COMMAND, out); + container.write(out); + } + + public interface Reader<T> { + T read(BitmessageAddress sender, InputStream in) throws IOException; + } + + private class SignatureCheckingInputStream extends InputStream { + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final InputStream wrapped; + + private SignatureCheckingInputStream(InputStream wrapped) { + this.wrapped = wrapped; + } + + @Override + public int read() throws IOException { + int read = wrapped.read(); + if (read >= 0) out.write(read); + return read; + } + + public void checkSignature(Pubkey pubkey) throws IOException, IllegalStateException { + if (!security().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { + throw new IllegalStateException("Signature check failed"); + } + } + } +} diff --git a/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java b/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java new file mode 100644 index 0000000..d661c50 --- /dev/null +++ b/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.extensions.pow; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.extensions.CryptoCustomMessage; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +import static ch.dissem.bitmessage.utils.Decode.*; + +/** + * @author Christian Basler + */ +public class ProofOfWorkRequest implements Streamable { + private static final long serialVersionUID = 4729003751499662713L; + + private final BitmessageAddress sender; + private final byte[] initialHash; + private final Request request; + + private final byte[] data; + + public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request) { + this(sender, initialHash, request, new byte[0]); + } + + public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request, byte[] data) { + this.sender = sender; + this.initialHash = initialHash; + this.request = request; + this.data = data; + } + + public static ProofOfWorkRequest read(BitmessageAddress client, InputStream in) throws IOException { + return new ProofOfWorkRequest( + client, + bytes(in, 64), + Request.valueOf(varString(in)), + varBytes(in) + ); + } + + public BitmessageAddress getSender() { + return sender; + } + + public byte[] getInitialHash() { + return initialHash; + } + + public Request getRequest() { + return request; + } + + public byte[] getData() { + return data; + } + + @Override + public void write(OutputStream out) throws IOException { + out.write(initialHash); + Encode.varString(request.name(), out); + Encode.varBytes(data, out); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ProofOfWorkRequest other = (ProofOfWorkRequest) o; + + if (!sender.equals(other.sender)) return false; + if (!Arrays.equals(initialHash, other.initialHash)) return false; + if (request != other.request) return false; + return Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + int result = sender.hashCode(); + result = 31 * result + Arrays.hashCode(initialHash); + result = 31 * result + request.hashCode(); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + public static class Reader implements CryptoCustomMessage.Reader<ProofOfWorkRequest> { + private final BitmessageAddress identity; + + public Reader(BitmessageAddress identity) { + this.identity = identity; + } + + @Override + public ProofOfWorkRequest read(BitmessageAddress sender, InputStream in) throws IOException { + return ProofOfWorkRequest.read(identity, in); + } + } + + public enum Request { + CALCULATE, + CALCULATING, + COMPLETE + } +} diff --git a/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java b/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java new file mode 100644 index 0000000..c1303e3 --- /dev/null +++ b/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.extensions; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.payload.GenericPayload; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; +import ch.dissem.bitmessage.utils.TestBase; +import ch.dissem.bitmessage.utils.TestUtils; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import static ch.dissem.bitmessage.utils.Singleton.security; +import static org.junit.Assert.assertEquals; + +public class CryptoCustomMessageTest extends TestBase { + @Test + public void ensureEncryptThenDecryptYieldsSameObject() throws Exception { + PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); + BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey); + + GenericPayload payloadBefore = new GenericPayload(0, 1, security().randomBytes(100)); + CryptoCustomMessage<GenericPayload> messageBefore = new CryptoCustomMessage<>(payloadBefore); + messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey())); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + messageBefore.write(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + + CustomMessage customMessage = CustomMessage.read(in, out.size()); + CryptoCustomMessage<GenericPayload> messageAfter = CryptoCustomMessage.read(customMessage, + new CryptoCustomMessage.Reader<GenericPayload>() { + @Override + public GenericPayload read(BitmessageAddress ignore, InputStream in) throws IOException { + return GenericPayload.read(0, in, 1, 100); + } + }); + GenericPayload payloadAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey()); + + assertEquals(payloadBefore, payloadAfter); + } + + @Test + public void testWithActualRequest() throws Exception { + PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); + final BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey); + + ProofOfWorkRequest requestBefore = new ProofOfWorkRequest(sendingIdentity, security().randomBytes(64), + ProofOfWorkRequest.Request.CALCULATE); + + CryptoCustomMessage<ProofOfWorkRequest> messageBefore = new CryptoCustomMessage<>(requestBefore); + messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey())); + + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + messageBefore.write(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + + CustomMessage customMessage = CustomMessage.read(in, out.size()); + CryptoCustomMessage<ProofOfWorkRequest> messageAfter = CryptoCustomMessage.read(customMessage, + new ProofOfWorkRequest.Reader(sendingIdentity)); + ProofOfWorkRequest requestAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey()); + + assertEquals(requestBefore, requestAfter); + } +} diff --git a/gradle.properties b/gradle.properties index eb6ed30..bb8beb4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,3 +8,4 @@ signing.password= ossrhUsername= ossrhPassword= +systemTestsEnabled=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 66e6c70..94f382d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip diff --git a/networking/build.gradle b/networking/build.gradle index 06bd5d0..984f585 100644 --- a/networking/build.gradle +++ b/networking/build.gradle @@ -11,10 +11,10 @@ uploadArchives { } dependencies { - compile project(':domain') + compile project(':core') testCompile 'junit:junit:4.11' testCompile 'org.slf4j:slf4j-simple:1.7.12' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile project(path: ':domain', configuration: 'testArtifacts') - testCompile project(':security-bc') + testCompile project(path: ':core', configuration: 'testArtifacts') + testCompile project(':cryptography-bc') } \ No newline at end of file diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java b/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java index 8ed2fb4..54c8acf 100644 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java @@ -41,7 +41,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; +import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; +import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT; +import static ch.dissem.bitmessage.networking.Connection.Mode.SYNC; import static ch.dissem.bitmessage.networking.Connection.State.*; import static ch.dissem.bitmessage.utils.Singleton.security; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; @@ -49,7 +52,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; /** * A connection to a specific node */ -public class Connection { +class Connection { public static final int READ_TIMEOUT = 2000; private static final Logger LOG = LoggerFactory.getLogger(Connection.class); private static final int CONNECT_TIMEOUT = 5000; @@ -63,10 +66,12 @@ public class Connection { private final NetworkAddress host; private final NetworkAddress node; private final Queue<MessagePayload> sendingQueue = new ConcurrentLinkedDeque<>(); - private final Map<InventoryVector, Long> requestedObjects; + private final Set<InventoryVector> commonRequestedObjects; + private final Set<InventoryVector> requestedObjects; private final long syncTimeout; private final ReaderRunnable reader = new ReaderRunnable(); private final WriterRunnable writer = new WriterRunnable(); + private final DefaultNetworkHandler networkHandler; private volatile State state; private InputStream in; @@ -75,39 +80,45 @@ public class Connection { private long[] streams; private int readTimeoutCounter; private boolean socketInitialized; + private long lastObjectTime; public Connection(InternalContext context, Mode mode, Socket socket, MessageListener listener, - ConcurrentMap<InventoryVector, Long> requestedObjectsMap) throws IOException { + Set<InventoryVector> requestedObjectsMap) throws IOException { this(context, mode, listener, socket, requestedObjectsMap, + Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)), new NetworkAddress.Builder().ip(socket.getInetAddress()).port(socket.getPort()).stream(1).build(), 0); } public Connection(InternalContext context, Mode mode, NetworkAddress node, MessageListener listener, - ConcurrentMap<InventoryVector, Long> requestedObjectsMap) { + Set<InventoryVector> requestedObjectsMap) { this(context, mode, listener, new Socket(), requestedObjectsMap, + Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)), node, 0); } private Connection(InternalContext context, Mode mode, MessageListener listener, Socket socket, - Map<InventoryVector, Long> requestedObjectsMap, NetworkAddress node, long syncTimeout) { + Set<InventoryVector> commonRequestedObjects, Set<InventoryVector> requestedObjects, NetworkAddress node, long syncTimeout) { this.startTime = UnixTime.now(); this.ctx = context; this.mode = mode; this.state = CONNECTING; this.listener = listener; this.socket = socket; - this.requestedObjects = requestedObjectsMap; + this.commonRequestedObjects = commonRequestedObjects; + this.requestedObjects = requestedObjects; this.host = new NetworkAddress.Builder().ipv6(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).port(0).build(); this.node = node; this.syncTimeout = (syncTimeout > 0 ? UnixTime.now(+syncTimeout) : 0); this.ivCache = new ConcurrentHashMap<>(); + this.networkHandler = (DefaultNetworkHandler) ctx.getNetworkHandler(); } public static Connection sync(InternalContext ctx, InetAddress address, int port, MessageListener listener, long timeoutInSeconds) throws IOException { - return new Connection(ctx, Mode.CLIENT, listener, new Socket(address, port), - new HashMap<InventoryVector, Long>(), + return new Connection(ctx, SYNC, listener, new Socket(address, port), + new HashSet<InventoryVector>(), + new HashSet<InventoryVector>(), new NetworkAddress.Builder().ip(address).port(port).stream(1).build(), timeoutInSeconds); } @@ -130,10 +141,13 @@ public class Connection { @SuppressWarnings("RedundantIfStatement") private boolean syncFinished(NetworkMessage msg) { + if (mode != SYNC) { + return false; + } if (Thread.interrupted()) { return true; } - if (syncTimeout == 0 || state != ACTIVE) { + if (state != ACTIVE) { return false; } if (syncTimeout < UnixTime.now()) { @@ -141,25 +155,26 @@ public class Connection { return true; } if (msg == null) { + if (requestedObjects.isEmpty() && sendingQueue.isEmpty()) + return true; + readTimeoutCounter++; return readTimeoutCounter > 1; + } else { + readTimeoutCounter = 0; + return false; } - readTimeoutCounter = 0; - if (!(msg.getPayload() instanceof Addr) && !(msg.getPayload() instanceof GetData) - && requestedObjects.isEmpty() && sendingQueue.isEmpty()) { - LOG.info("Synchronisation completed"); - return true; - } - return false; } private void activateConnection() { LOG.info("Successfully established connection with node " + node); state = ACTIVE; - sendAddresses(); + if (mode != SYNC) { + sendAddresses(); + ctx.getNodeRegistry().offerAddresses(Collections.singletonList(node)); + } sendInventory(); node.setTime(UnixTime.now()); - ctx.getNodeRegistry().offerAddresses(Collections.singletonList(node)); } private void cleanupIvCache() { @@ -187,84 +202,75 @@ public class Connection { } } - private void updateRequestedObjects(List<InventoryVector> missing) { - Long now = UnixTime.now(); - Long fiveMinutesAgo = now - 5 * MINUTE; - Long tenMinutesAgo = now - 10 * MINUTE; - List<InventoryVector> stillMissing = new LinkedList<>(); - for (Map.Entry<InventoryVector, Long> entry : requestedObjects.entrySet()) { - if (entry.getValue() < fiveMinutesAgo) { - stillMissing.add(entry.getKey()); - // If it's still not available after 10 minutes, we won't look for it - // any longer (except it's announced again) - if (entry.getValue() < tenMinutesAgo) { - requestedObjects.remove(entry.getKey()); - } - } - } - - for (InventoryVector iv : missing) { - requestedObjects.put(iv, now); - } - if (!stillMissing.isEmpty()) { - LOG.debug(stillMissing.size() + " items are still missing."); - missing.addAll(stillMissing); - } - } - private void receiveMessage(MessagePayload messagePayload) { switch (messagePayload.getCommand()) { case INV: - Inv inv = (Inv) messagePayload; - updateIvCache(inv.getInventory()); - List<InventoryVector> missing = ctx.getInventory().getMissing(inv.getInventory(), streams); - missing.removeAll(requestedObjects.keySet()); - LOG.debug("Received inventory with " + inv.getInventory().size() + " elements, of which are " - + missing.size() + " missing."); - updateRequestedObjects(missing); - send(new GetData.Builder().inventory(missing).build()); + receiveMessage((Inv) messagePayload); break; case GETDATA: - GetData getData = (GetData) messagePayload; - for (InventoryVector iv : getData.getInventory()) { - ObjectMessage om = ctx.getInventory().getObject(iv); - if (om != null) sendingQueue.offer(om); - } + receiveMessage((GetData) messagePayload); break; case OBJECT: - ObjectMessage objectMessage = (ObjectMessage) messagePayload; - try { - if (ctx.getInventory().contains(objectMessage)) { - LOG.trace("Received object " + objectMessage.getInventoryVector() + " - already in inventory"); - break; - } - security().checkProofOfWork(objectMessage, ctx.getNetworkNonceTrialsPerByte(), ctx.getNetworkExtraBytes()); - listener.receive(objectMessage); - ctx.getInventory().storeObject(objectMessage); - // offer object to some random nodes so it gets distributed throughout the network: - // FIXME: don't do this while we catch up after initialising our first connection - // (that might be a bit tricky to do) - ctx.getNetworkHandler().offer(objectMessage.getInventoryVector()); - } catch (InsufficientProofOfWorkException e) { - LOG.warn(e.getMessage()); - // DebugUtils.saveToFile(objectMessage); // this line must not be committed active - } catch (IOException e) { - LOG.error("Stream " + objectMessage.getStream() + ", object type " + objectMessage.getType() + ": " + e.getMessage(), e); - } finally { - requestedObjects.remove(objectMessage.getInventoryVector()); - } + receiveMessage((ObjectMessage) messagePayload); break; case ADDR: - Addr addr = (Addr) messagePayload; - LOG.debug("Received " + addr.getAddresses().size() + " addresses."); - ctx.getNodeRegistry().offerAddresses(addr.getAddresses()); + receiveMessage((Addr) messagePayload); break; + case CUSTOM: case VERACK: case VERSION: - throw new RuntimeException("Unexpectedly received '" + messagePayload.getCommand() + "' command"); + default: + throw new IllegalStateException("Unexpectedly received '" + messagePayload.getCommand() + "' command"); } } + private void receiveMessage(Inv inv) { + int originalSize = inv.getInventory().size(); + updateIvCache(inv.getInventory()); + List<InventoryVector> missing = ctx.getInventory().getMissing(inv.getInventory(), streams); + missing.removeAll(commonRequestedObjects); + LOG.debug("Received inventory with " + originalSize + " elements, of which are " + + missing.size() + " missing."); + send(new GetData.Builder().inventory(missing).build()); + } + + private void receiveMessage(GetData getData) { + for (InventoryVector iv : getData.getInventory()) { + ObjectMessage om = ctx.getInventory().getObject(iv); + if (om != null) sendingQueue.offer(om); + } + } + + private void receiveMessage(ObjectMessage objectMessage) { + requestedObjects.remove(objectMessage.getInventoryVector()); + if (ctx.getInventory().contains(objectMessage)) { + LOG.trace("Received object " + objectMessage.getInventoryVector() + " - already in inventory"); + return; + } + try { + listener.receive(objectMessage); + security().checkProofOfWork(objectMessage, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES); + ctx.getInventory().storeObject(objectMessage); + // offer object to some random nodes so it gets distributed throughout the network: + networkHandler.offer(objectMessage.getInventoryVector()); + lastObjectTime = UnixTime.now(); + } catch (InsufficientProofOfWorkException e) { + LOG.warn(e.getMessage()); + // DebugUtils.saveToFile(objectMessage); // this line must not be committed active + } catch (IOException e) { + LOG.error("Stream " + objectMessage.getStream() + ", object type " + objectMessage.getType() + ": " + e.getMessage(), e); + } finally { + if (commonRequestedObjects.remove(objectMessage.getInventoryVector())) { + LOG.debug("Received object that wasn't requested."); + } + } + } + + private void receiveMessage(Addr addr) { + LOG.debug("Received " + addr.getAddresses().size() + " addresses."); + ctx.getNodeRegistry().offerAddresses(addr.getAddresses()); + } + private void sendAddresses() { List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses(1000, streams); sendingQueue.offer(new Addr.Builder().addresses(addresses).build()); @@ -281,11 +287,19 @@ public class Connection { public void disconnect() { state = DISCONNECTED; + + // Make sure objects that are still missing are requested from other nodes + networkHandler.request(requestedObjects); } - private void send(MessagePayload payload) { + void send(MessagePayload payload) { try { - new NetworkMessage(payload).write(out); + if (payload instanceof GetData) { + requestedObjects.addAll(((GetData) payload).getInventory()); + } + synchronized (this) { + new NetworkMessage(payload).write(out); + } } catch (IOException e) { LOG.error(e.getMessage(), e); disconnect(); @@ -337,79 +351,31 @@ public class Connection { return writer; } - public enum Mode {SERVER, CLIENT} + public enum Mode {SERVER, CLIENT, SYNC} public enum State {CONNECTING, ACTIVE, DISCONNECTED} public class ReaderRunnable implements Runnable { @Override public void run() { + lastObjectTime = 0; try (Socket socket = Connection.this.socket) { initSocket(socket); - if (mode == CLIENT) { + if (mode == CLIENT || mode == SYNC) { send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); } while (state != DISCONNECTED) { - Thread.sleep(100); - try { - NetworkMessage msg = Factory.getNetworkMessage(version, in); - if (msg == null) - continue; - switch (state) { - case ACTIVE: - receiveMessage(msg.getPayload()); - break; - - default: - switch (msg.getPayload().getCommand()) { - case VERSION: - Version payload = (Version) msg.getPayload(); - if (payload.getNonce() == ctx.getClientNonce()) { - LOG.info("Tried to connect to self, disconnecting."); - disconnect(); - } else if (payload.getVersion() >= BitmessageContext.CURRENT_VERSION) { - version = payload.getVersion(); - streams = payload.getStreams(); - send(new VerAck()); - switch (mode) { - case SERVER: - send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); - break; - case CLIENT: - activateConnection(); - break; - } - } else { - LOG.info("Received unsupported version " + payload.getVersion() + ", disconnecting."); - disconnect(); - } - break; - case VERACK: - switch (mode) { - case SERVER: - activateConnection(); - break; - case CLIENT: - // NO OP - break; - } - break; - default: - throw new NodeException("Command 'version' or 'verack' expected, but was '" - + msg.getPayload().getCommand() + "'"); - } - } - if (socket.isClosed() || syncFinished(msg)) disconnect(); - } catch (SocketTimeoutException ignore) { - if (state == ACTIVE) { - if (syncFinished(null)) disconnect(); + if (mode != SYNC) { + if (state == ACTIVE && requestedObjects.isEmpty() && sendingQueue.isEmpty()) { + Thread.sleep(1000); + } else { + Thread.sleep(100); } } + receive(); } - } catch (InterruptedException | IOException | NodeException e) { + } catch (Exception e) { LOG.trace("Reader disconnected from node " + node + ": " + e.getMessage()); - } catch (RuntimeException e) { - LOG.trace("Reader disconnecting from node " + node + " due to error: " + e.getMessage(), e); } finally { disconnect(); try { @@ -419,6 +385,85 @@ public class Connection { } } } + + private void receive() throws InterruptedException { + try { + NetworkMessage msg = Factory.getNetworkMessage(version, in); + if (msg == null) + return; + switch (state) { + case ACTIVE: + receiveMessage(msg.getPayload()); + break; + + default: + handleCommand(msg.getPayload()); + break; + } + if (socket.isClosed() || syncFinished(msg) || checkOpenRequests()) disconnect(); + } catch (SocketTimeoutException ignore) { + if (state == ACTIVE && syncFinished(null)) disconnect(); + } + } + + private void handleCommand(MessagePayload payload) { + switch (payload.getCommand()) { + case VERSION: + handleVersion((Version) payload); + break; + case VERACK: + switch (mode) { + case SERVER: + activateConnection(); + break; + case CLIENT: + case SYNC: + default: + // NO OP + break; + } + break; + case CUSTOM: + MessagePayload response = ctx.getCustomCommandHandler().handle((CustomMessage) payload); + if (response != null) { + send(response); + } + disconnect(); + break; + default: + throw new NodeException("Command 'version' or 'verack' expected, but was '" + + payload.getCommand() + "'"); + } + } + + private void handleVersion(Version version) { + if (version.getNonce() == ctx.getClientNonce()) { + LOG.info("Tried to connect to self, disconnecting."); + disconnect(); + } else if (version.getVersion() >= BitmessageContext.CURRENT_VERSION) { + Connection.this.version = version.getVersion(); + streams = version.getStreams(); + send(new VerAck()); + switch (mode) { + case SERVER: + send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); + break; + case CLIENT: + case SYNC: + activateConnection(); + break; + default: + // NO OP + } + } else { + LOG.info("Received unsupported version " + version.getVersion() + ", disconnecting."); + disconnect(); + } + } + } + + private boolean checkOpenRequests() { + return !requestedObjects.isEmpty() && lastObjectTime > 0 && (UnixTime.now() - lastObjectTime) > 2 * MINUTE; } public class WriterRunnable implements Runnable { @@ -427,10 +472,10 @@ public class Connection { try (Socket socket = Connection.this.socket) { initSocket(socket); while (state != DISCONNECTED) { - if (sendingQueue.size() > 0) { - send(sendingQueue.poll()); + if (sendingQueue.isEmpty()) { + Thread.sleep(1000); } else { - Thread.sleep(100); + send(sendingQueue.poll()); } } } catch (IOException | InterruptedException e) { diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java b/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java new file mode 100644 index 0000000..1163d1c --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.networking; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.NetworkHandler; +import ch.dissem.bitmessage.utils.UnixTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; +import java.util.List; + +import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT; +import static ch.dissem.bitmessage.networking.DefaultNetworkHandler.NETWORK_MAGIC_NUMBER; + +/** + * @author Christian Basler + */ +public class ConnectionOrganizer implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(ConnectionOrganizer.class); + + private final InternalContext ctx; + private final DefaultNetworkHandler networkHandler; + private final NetworkHandler.MessageListener listener; + + private Connection initialConnection; + + public ConnectionOrganizer(InternalContext ctx, + DefaultNetworkHandler networkHandler, + NetworkHandler.MessageListener listener) { + this.ctx = ctx; + this.networkHandler = networkHandler; + this.listener = listener; + } + + @Override + public void run() { + try { + while (networkHandler.isRunning()) { + try { + int active = 0; + long now = UnixTime.now(); + + int diff = networkHandler.connections.size() - ctx.getConnectionLimit(); + if (diff > 0) { + for (Connection c : networkHandler.connections) { + c.disconnect(); + diff--; + if (diff == 0) break; + } + } + boolean forcedDisconnect = false; + for (Iterator<Connection> iterator = networkHandler.connections.iterator(); iterator.hasNext(); ) { + Connection c = iterator.next(); + // Just in case they were all created at the same time, don't disconnect + // all at once. + if (!forcedDisconnect && now - c.getStartTime() > ctx.getConnectionTTL()) { + c.disconnect(); + forcedDisconnect = true; + } + switch (c.getState()) { + case DISCONNECTED: + iterator.remove(); + break; + case ACTIVE: + active++; + break; + default: + // nothing to do + } + } + + if (active < NETWORK_MAGIC_NUMBER) { + List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses( + NETWORK_MAGIC_NUMBER - active, ctx.getStreams()); + boolean first = active == 0 && initialConnection == null; + for (NetworkAddress address : addresses) { + Connection c = new Connection(ctx, CLIENT, address, listener, networkHandler.requestedObjects); + if (first) { + initialConnection = c; + first = false; + } + networkHandler.startConnection(c); + } + Thread.sleep(10000); + } else if (initialConnection == null) { + Thread.sleep(30000); + } else { + initialConnection.disconnect(); + initialConnection = null; + Thread.sleep(10000); + } + } catch (InterruptedException e) { + networkHandler.stop(); + } catch (Exception e) { + LOG.error("Error in connection manager. Ignored.", e); + } + } + } finally { + LOG.debug("Connection manager shutting down."); + networkHandler.stop(); + } + } +} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java index 3944378..43333ac 100644 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java @@ -18,41 +18,45 @@ package ch.dissem.bitmessage.networking; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.InternalContext.ContextHolder; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.GetData; +import ch.dissem.bitmessage.entity.NetworkMessage; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.exception.NodeException; +import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.NetworkHandler; import ch.dissem.bitmessage.utils.Collections; import ch.dissem.bitmessage.utils.Property; -import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; -import java.net.ServerSocket; import java.net.Socket; import java.util.*; import java.util.concurrent.*; -import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT; import static ch.dissem.bitmessage.networking.Connection.Mode.SERVER; import static ch.dissem.bitmessage.networking.Connection.State.ACTIVE; -import static ch.dissem.bitmessage.networking.Connection.State.DISCONNECTED; import static ch.dissem.bitmessage.utils.DebugUtils.inc; +import static java.util.Collections.newSetFromMap; /** * Handles all the networky stuff. */ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { - public final static int NETWORK_MAGIC_NUMBER = 8; private final static Logger LOG = LoggerFactory.getLogger(DefaultNetworkHandler.class); - private final List<Connection> connections = new LinkedList<>(); + + public final static int NETWORK_MAGIC_NUMBER = 8; + + final Collection<Connection> connections = new ConcurrentLinkedQueue<>(); private final ExecutorService pool; private InternalContext ctx; - private ServerSocket serverSocket; + private ServerRunnable server; private volatile boolean running; - private ConcurrentMap<InventoryVector, Long> requestedObjects = new ConcurrentHashMap<>(); + final Set<InventoryVector> requestedObjects = newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(50_000)); public DefaultNetworkHandler() { pool = Executors.newCachedThreadPool(new ThreadFactory() { @@ -71,14 +75,35 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { } @Override - public Future<?> synchronize(InetAddress trustedHost, int port, MessageListener listener, long timeoutInSeconds) { + public Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds) { try { - Connection connection = Connection.sync(ctx, trustedHost, port, listener, timeoutInSeconds); + Connection connection = Connection.sync(ctx, server, port, listener, timeoutInSeconds); Future<?> reader = pool.submit(connection.getReader()); pool.execute(connection.getWriter()); return reader; } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); + } + } + + @Override + public CustomMessage send(InetAddress server, int port, CustomMessage request) { + try (Socket socket = new Socket(server, port)) { + socket.setSoTimeout(Connection.READ_TIMEOUT); + new NetworkMessage(request).write(socket.getOutputStream()); + NetworkMessage networkMessage = Factory.getNetworkMessage(3, socket.getInputStream()); + if (networkMessage != null && networkMessage.getPayload() instanceof CustomMessage) { + return (CustomMessage) networkMessage.getPayload(); + } else { + if (networkMessage == null) { + throw new NodeException("No response from node " + server); + } else { + throw new NodeException("Unexpected response from node " + + server + ": " + networkMessage.getPayload().getCommand()); + } + } + } catch (IOException e) { + throw new ApplicationException(e); } } @@ -93,76 +118,11 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { try { running = true; connections.clear(); - serverSocket = new ServerSocket(ctx.getPort()); - pool.execute(new Runnable() { - @Override - public void run() { - while (!serverSocket.isClosed()) { - try { - Socket socket = serverSocket.accept(); - socket.setSoTimeout(Connection.READ_TIMEOUT); - startConnection(new Connection(ctx, SERVER, socket, listener, requestedObjects)); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - } - } - }); - pool.execute(new Runnable() { - @Override - public void run() { - try { - while (running) { - try { - int active = 0; - long now = UnixTime.now(); - synchronized (connections) { - int diff = connections.size() - ctx.getConnectionLimit(); - if (diff > 0) { - for (Connection c : connections) { - c.disconnect(); - diff--; - if (diff == 0) break; - } - } - for (Iterator<Connection> iterator = connections.iterator(); iterator.hasNext(); ) { - Connection c = iterator.next(); - if (now - c.getStartTime() > ctx.getConnectionTTL()) { - c.disconnect(); - } - if (c.getState() == DISCONNECTED) { - // Remove the current element from the iterator and the list. - iterator.remove(); - } - if (c.getState() == ACTIVE) { - active++; - } - } - } - if (active < NETWORK_MAGIC_NUMBER) { - List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses( - NETWORK_MAGIC_NUMBER - active, ctx.getStreams()); - for (NetworkAddress address : addresses) { - startConnection(new Connection(ctx, CLIENT, address, listener, requestedObjects)); - } - Thread.sleep(10000); - } else { - Thread.sleep(30000); - } - } catch (InterruptedException e) { - running = false; - } catch (Exception e) { - LOG.error("Error in connection manager. Ignored.", e); - } - } - } finally { - LOG.debug("Connection manager shutting down."); - running = false; - } - } - }); + server = new ServerRunnable(ctx, this, listener); + pool.execute(server); + pool.execute(new ConnectionOrganizer(ctx, this, listener)); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -173,21 +133,22 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { @Override public void stop() { - running = false; - try { - serverSocket.close(); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } + server.close(); synchronized (connections) { + running = false; for (Connection c : connections) { c.disconnect(); } } + requestedObjects.clear(); } - private void startConnection(Connection c) { + void startConnection(Connection c) { + if (!running) return; + synchronized (connections) { + if (!running) return; + // prevent connecting twice to the same node if (connections.contains(c)) { return; @@ -201,11 +162,9 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { @Override public void offer(final InventoryVector iv) { List<Connection> target = new LinkedList<>(); - synchronized (connections) { - for (Connection connection : connections) { - if (connection.getState() == ACTIVE && !connection.knowsOf(iv)) { - target.add(connection); - } + for (Connection connection : connections) { + if (connection.getState() == ACTIVE && !connection.knowsOf(iv)) { + target.add(connection); } } List<Connection> randomSubset = Collections.selectRandom(NETWORK_MAGIC_NUMBER, target); @@ -220,16 +179,14 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { TreeMap<Long, Integer> incomingConnections = new TreeMap<>(); TreeMap<Long, Integer> outgoingConnections = new TreeMap<>(); - synchronized (connections) { - for (Connection connection : connections) { - if (connection.getState() == ACTIVE) { - long stream = connection.getNode().getStream(); - streams.add(stream); - if (connection.getMode() == SERVER) { - inc(incomingConnections, stream); - } else { - inc(outgoingConnections, stream); - } + for (Connection connection : connections) { + if (connection.getState() == ACTIVE) { + long stream = connection.getNode().getStream(); + streams.add(stream); + if (connection.getMode() == SERVER) { + inc(incomingConnections, stream); + } else { + inc(outgoingConnections, stream); } } } @@ -247,7 +204,55 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { } return new Property("network", null, new Property("connectionManager", running ? "running" : "stopped"), - new Property("connections", null, streamProperties) + new Property("connections", null, streamProperties), + new Property("requestedObjects", requestedObjects.size()) ); } + + void request(Set<InventoryVector> inventoryVectors) { + if (!running || inventoryVectors.isEmpty()) return; + + Map<Connection, List<InventoryVector>> distribution = new HashMap<>(); + for (Connection connection : connections) { + if (connection.getState() == ACTIVE) { + distribution.put(connection, new LinkedList<InventoryVector>()); + } + } + Iterator<InventoryVector> iterator = inventoryVectors.iterator(); + if (!iterator.hasNext()) { + return; + } + InventoryVector next = iterator.next(); + Connection previous = null; + do { + for (Connection connection : distribution.keySet()) { + if (connection == previous) { + next = iterator.next(); + } + if (connection.knowsOf(next)) { + List<InventoryVector> ivs = distribution.get(connection); + if (ivs.size() == GetData.MAX_INVENTORY_SIZE) { + connection.send(new GetData.Builder().inventory(ivs).build()); + ivs.clear(); + } + ivs.add(next); + iterator.remove(); + + if (iterator.hasNext()) { + next = iterator.next(); + previous = connection; + } else { + break; + } + } + } + } while (iterator.hasNext()); + + for (Connection connection : distribution.keySet()) { + List<InventoryVector> ivs = distribution.get(connection); + if (!ivs.isEmpty()) { + connection.send(new GetData.Builder().inventory(ivs).build()); + } + } + } } diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java b/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java new file mode 100644 index 0000000..bf6d6f6 --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.networking; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.ports.NetworkHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import static ch.dissem.bitmessage.networking.Connection.Mode.SERVER; + +/** + * @author Christian Basler + */ +public class ServerRunnable implements Runnable, Closeable { + private static final Logger LOG = LoggerFactory.getLogger(ServerRunnable.class); + private final InternalContext ctx; + private final ServerSocket serverSocket; + private final DefaultNetworkHandler networkHandler; + private final NetworkHandler.MessageListener listener; + + public ServerRunnable(InternalContext ctx, DefaultNetworkHandler networkHandler, NetworkHandler.MessageListener listener) throws IOException { + this.ctx = ctx; + this.networkHandler = networkHandler; + this.listener = listener; + this.serverSocket = new ServerSocket(ctx.getPort()); + } + + @Override + public void run() { + while (!serverSocket.isClosed()) { + try { + Socket socket = serverSocket.accept(); + socket.setSoTimeout(Connection.READ_TIMEOUT); + networkHandler.startConnection(new Connection(ctx, SERVER, socket, listener, + networkHandler.requestedObjects)); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); + } + } + } + + @Override + public void close() { + try { + serverSocket.close(); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); + } + } +} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java index 9fb2ea5..accb4c5 100644 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java @@ -21,12 +21,12 @@ import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.NetworkHandler; -import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.utils.Property; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.Mockito; import java.net.InetAddress; import java.util.concurrent.Future; @@ -51,28 +51,30 @@ public class NetworkHandlerTest { public static void setUp() { peerInventory = new TestInventory(); peer = new BitmessageContext.Builder() - .addressRepo(Mockito.mock(AddressRepository.class)) + .addressRepo(mock(AddressRepository.class)) .inventory(peerInventory) - .messageRepo(Mockito.mock(MessageRepository.class)) + .messageRepo(mock(MessageRepository.class)) + .powRepo(mock(ProofOfWorkRepository.class)) .port(6001) .nodeRegistry(new TestNodeRegistry()) .networkHandler(new DefaultNetworkHandler()) - .security(new BouncySecurity()) - .listener(Mockito.mock(BitmessageContext.Listener.class)) + .cryptography(new BouncyCryptography()) + .listener(mock(BitmessageContext.Listener.class)) .build(); peer.startup(); nodeInventory = new TestInventory(); networkHandler = new DefaultNetworkHandler(); node = new BitmessageContext.Builder() - .addressRepo(Mockito.mock(AddressRepository.class)) + .addressRepo(mock(AddressRepository.class)) .inventory(nodeInventory) - .messageRepo(Mockito.mock(MessageRepository.class)) + .messageRepo(mock(MessageRepository.class)) + .powRepo(mock(ProofOfWorkRepository.class)) .port(6002) .nodeRegistry(new TestNodeRegistry(localhost)) .networkHandler(networkHandler) - .security(new BouncySecurity()) - .listener(Mockito.mock(BitmessageContext.Listener.class)) + .cryptography(new BouncyCryptography()) + .listener(mock(BitmessageContext.Listener.class)) .build(); } @@ -91,14 +93,14 @@ public class NetworkHandlerTest { } while (node.isRunning()); } - @Test(timeout = 20_000) + @Test(timeout = 5_000) public void ensureNodesAreConnecting() { try { node.startup(); Property status; do { Thread.yield(); - status = node.status().getProperty("network").getProperty("connections").getProperty("stream 0"); + status = node.status().getProperty("network", "connections", "stream 0"); } while (status == null); assertEquals(1, status.getProperty("outgoing").getValue()); } finally { @@ -114,7 +116,8 @@ public class NetworkHandlerTest { ); nodeInventory.init( - "V1Msg.payload" + "V1Msg.payload", + "V4Pubkey.payload" ); Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, @@ -125,7 +128,7 @@ public class NetworkHandlerTest { assertInventorySize(3, peerInventory); } - @Test(timeout = 10_000) + @Test(timeout = 5_000) public void ensureObjectsAreSynchronizedIfOnlyPeerHasObjects() throws Exception { peerInventory.init( "V4Pubkey.payload", @@ -142,7 +145,7 @@ public class NetworkHandlerTest { assertInventorySize(2, peerInventory); } - @Test(timeout = 10_000) + @Test(timeout = 5_000) public void ensureObjectsAreSynchronizedIfOnlyNodeHasObjects() throws Exception { peerInventory.init(); diff --git a/repositories/build.gradle b/repositories/build.gradle index fecebce..abb8651 100644 --- a/repositories/build.gradle +++ b/repositories/build.gradle @@ -2,7 +2,7 @@ uploadArchives { repositories { mavenDeployer { pom.project { - name 'Jabit Domain' + name 'Jabit Repositories' artifactId = 'jabit-repositories' description 'A Java implementation of the Bitmessage protocol. This contains JDBC implementations of the repositories.' } @@ -13,10 +13,10 @@ uploadArchives { sourceCompatibility = 1.8 dependencies { - compile project(':domain') + compile project(':core') compile 'org.flywaydb:flyway-core:3.2.1' testCompile 'junit:junit:4.12' testCompile 'com.h2database:h2:1.4.190' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile project(':security-bc') + testCompile project(':cryptography-bc') } \ No newline at end of file diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java index 337d50a..5422997 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java @@ -26,7 +26,6 @@ import ch.dissem.bitmessage.ports.AddressRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -71,6 +70,11 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito return find("private_key IS NOT NULL"); } + @Override + public List<BitmessageAddress> getChans() { + return find("chan = '1'"); + } + @Override public List<BitmessageAddress> getSubscriptions() { return find("subscribed = '1'"); @@ -87,22 +91,22 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito @Override public List<BitmessageAddress> getContacts() { - return find("private_key IS NULL"); + return find("private_key IS NULL OR chan = '1'"); } private List<BitmessageAddress> find(String where) { List<BitmessageAddress> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed FROM Address WHERE " + where); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed, chan " + + "FROM Address WHERE " + where) + ) { while (rs.next()) { BitmessageAddress address; InputStream privateKeyStream = rs.getBinaryStream("private_key"); - if (privateKeyStream != null) { - PrivateKey privateKey = PrivateKey.read(privateKeyStream); - address = new BitmessageAddress(privateKey); - } else { + if (privateKeyStream == null) { address = new BitmessageAddress(rs.getString("address")); Blob publicKeyBlob = rs.getBlob("public_key"); if (publicKeyBlob != null) { @@ -113,9 +117,13 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito } address.setPubkey(pubkey); } + } else { + PrivateKey privateKey = PrivateKey.read(privateKeyStream); + address = new BitmessageAddress(privateKey); } address.setAlias(rs.getString("alias")); address.setSubscribed(rs.getBoolean("subscribed")); + address.setChan(rs.getBoolean("chan")); result.add(address); } @@ -126,11 +134,15 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito } private boolean exists(BitmessageAddress address) { - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'"); - rs.next(); - return rs.getInt(1) > 0; + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Address " + + "WHERE address='" + address.getAddress() + "'") + ) { + if (rs.next()) { + return rs.getInt(1) > 0; + } } catch (SQLException e) { LOG.error(e.getMessage(), e); } @@ -151,28 +163,47 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito } private void update(BitmessageAddress address) throws IOException, SQLException { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement( - "UPDATE Address SET alias=?, public_key=?, private_key=?, subscribed=? WHERE address=?"); - ps.setString(1, address.getAlias()); - writePubkey(ps, 2, address.getPubkey()); - writeBlob(ps, 3, address.getPrivateKey()); - ps.setBoolean(4, address.isSubscribed()); - ps.setString(5, address.getAddress()); + StringBuilder statement = new StringBuilder("UPDATE Address SET alias=?"); + if (address.getPubkey() != null) { + statement.append(", public_key=?"); + } + if (address.getPrivateKey() != null) { + statement.append(", private_key=?"); + } + statement.append(", subscribed=?, chan=? WHERE address=?"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement(statement.toString()) + ) { + int i = 0; + ps.setString(++i, address.getAlias()); + if (address.getPubkey() != null) { + writePubkey(ps, ++i, address.getPubkey()); + } + if (address.getPrivateKey() != null) { + writeBlob(ps, ++i, address.getPrivateKey()); + } + ps.setBoolean(++i, address.isSubscribed()); + ps.setBoolean(++i, address.isChan()); + ps.setString(++i, address.getAddress()); ps.executeUpdate(); } } private void insert(BitmessageAddress address) throws IOException, SQLException { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Address (address, version, alias, public_key, private_key, subscribed) VALUES (?, ?, ?, ?, ?, ?)"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO Address (address, version, alias, public_key, private_key, subscribed, chan) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)") + ) { ps.setString(1, address.getAddress()); ps.setLong(2, address.getVersion()); ps.setString(3, address.getAlias()); writePubkey(ps, 4, address.getPubkey()); writeBlob(ps, 5, address.getPrivateKey()); ps.setBoolean(6, address.isSubscribed()); + ps.setBoolean(7, address.isChan()); ps.executeUpdate(); } } @@ -189,8 +220,10 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito @Override public void remove(BitmessageAddress address) { - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement() + ) { stmt.executeUpdate("DELETE FROM Address WHERE address = '" + address.getAddress() + "'"); } catch (SQLException e) { LOG.error(e.getMessage(), e); diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java index 601ce29..1df12f6 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java @@ -31,7 +31,7 @@ import static ch.dissem.bitmessage.utils.Strings.hex; /** * Helper class that does Flyway migration, provides JDBC connections and some helper methods. */ -abstract class JdbcHelper { +public abstract class JdbcHelper { private static final Logger LOG = LoggerFactory.getLogger(JdbcHelper.class); protected final JdbcConfig config; @@ -77,12 +77,12 @@ abstract class JdbcHelper { } protected void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException { - if (data != null) { + if (data == null) { + ps.setBytes(parameterIndex, null); + } else { ByteArrayOutputStream os = new ByteArrayOutputStream(); data.write(os); ps.setBytes(parameterIndex, os.toByteArray()); - } else { - ps.setBytes(parameterIndex, null); } } } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java index 9c9c9e7..0cc9f3b 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java @@ -19,6 +19,7 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.Inventory; import org.slf4j.Logger; @@ -60,11 +61,12 @@ public class JdbcInventory extends JdbcHelper implements Inventory { if (cache.get(stream) == null) { result = new ConcurrentHashMap<>(); cache.put(stream, result); - - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT hash, expires FROM Inventory WHERE expires > " - + now(-5 * MINUTE) + " AND stream = " + stream); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT hash, expires FROM Inventory " + + "WHERE expires > " + now(-5 * MINUTE) + " AND stream = " + stream) + ) { while (rs.next()) { result.put(new InventoryVector(rs.getBytes("hash")), rs.getLong("expires")); } @@ -80,16 +82,18 @@ public class JdbcInventory extends JdbcHelper implements Inventory { @Override public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { for (long stream : streams) { - getCache(stream).forEach((iv, t) -> offer.remove(iv)); + offer.removeAll(getCache(stream).keySet()); } return offer; } @Override public ObjectMessage getObject(InventoryVector vector) { - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT data, version FROM Inventory WHERE hash = X'" + vector + "'"); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT data, version FROM Inventory WHERE hash = X'" + vector + "'") + ) { if (rs.next()) { Blob data = rs.getBlob("data"); return Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()); @@ -99,25 +103,27 @@ public class JdbcInventory extends JdbcHelper implements Inventory { } } catch (Exception e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @Override public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) { - try (Connection connection = config.getConnection()) { - StringBuilder query = new StringBuilder("SELECT data, version FROM Inventory WHERE 1=1"); - if (stream > 0) { - query.append(" AND stream = ").append(stream); - } - if (version > 0) { - query.append(" AND version = ").append(version); - } - if (types.length > 0) { - query.append(" AND type IN (").append(join(types)).append(")"); - } - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(query.toString()); + StringBuilder query = new StringBuilder("SELECT data, version FROM Inventory WHERE 1=1"); + if (stream > 0) { + query.append(" AND stream = ").append(stream); + } + if (version > 0) { + query.append(" AND version = ").append(version); + } + if (types.length > 0) { + query.append(" AND type IN (").append(join(types)).append(')'); + } + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query.toString()) + ) { List<ObjectMessage> result = new LinkedList<>(); while (rs.next()) { Blob data = rs.getBlob("data"); @@ -126,14 +132,20 @@ public class JdbcInventory extends JdbcHelper implements Inventory { return result; } catch (Exception e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @Override public void storeObject(ObjectMessage object) { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement("INSERT INTO Inventory (hash, stream, expires, data, type, version) VALUES (?, ?, ?, ?, ?, ?)"); + if (getCache(object.getStream()).containsKey(object.getInventoryVector())) + return; + + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("INSERT INTO Inventory " + + "(hash, stream, expires, data, type, version) VALUES (?, ?, ?, ?, ?, ?)") + ) { InventoryVector iv = object.getInventoryVector(); LOG.trace("Storing object " + iv); ps.setBytes(1, iv.getHash()); @@ -159,8 +171,11 @@ public class JdbcInventory extends JdbcHelper implements Inventory { @Override public void cleanup() { - try (Connection connection = config.getConnection()) { - connection.createStatement().executeUpdate("DELETE FROM Inventory WHERE expires < " + now(-5 * MINUTE)); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement() + ) { + stmt.executeUpdate("DELETE FROM Inventory WHERE expires < " + now(-5 * MINUTE)); } catch (SQLException e) { LOG.debug(e.getMessage(), e); } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java index 4599b79..e043e7e 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -21,7 +21,9 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,14 +47,16 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito @Override public List<Label> getLabels() { List<Label> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label ORDER BY ord"); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label ORDER BY ord") + ) { while (rs.next()) { result.add(getLabel(rs)); } } catch (SQLException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } return result; } @@ -72,10 +76,12 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito @Override public List<Label> getLabels(Label.Type... types) { List<Label> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE type IN (" + join(types) + - ") ORDER BY ord"); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE type IN (" + join(types) + + ") ORDER BY ord") + ) { while (rs.next()) { result.add(getLabel(rs)); } @@ -88,17 +94,19 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito @Override public int countUnread(Label label) { String where; - if (label != null) { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND "; - } else { + if (label == null) { where = ""; + } else { + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND "; } where += "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))"; - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where) + ) { if (rs.next()) { return rs.getInt(1); } @@ -108,6 +116,20 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito return 0; } + @Override + public Plaintext getMessage(byte[] initialHash) { + List<Plaintext> plaintexts = find("initial_hash=X'" + Strings.hex(initialHash) + "'"); + switch (plaintexts.size()) { + case 0: + return null; + case 1: + return plaintexts.get(0); + default: + throw new ApplicationException("This shouldn't happen, found " + plaintexts.size() + + " messages, one or none was expected"); + } + } + @Override public List<Plaintext> findMessages(Label label) { return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"); @@ -130,9 +152,12 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito private List<Plaintext> find(String where) { List<Plaintext> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, iv, type, sender, recipient, data, sent, received, status FROM Message WHERE " + where); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, iv, type, sender, recipient, data, sent, received, status " + + "FROM Message WHERE " + where) + ) { while (rs.next()) { byte[] iv = rs.getBytes("iv"); InputStream data = rs.getBinaryStream("data"); @@ -141,8 +166,8 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito long id = rs.getLong("id"); builder.id(id); builder.IV(new InventoryVector(iv)); - builder.from(ctx.getAddressRepo().getAddress(rs.getString("sender"))); - builder.to(ctx.getAddressRepo().getAddress(rs.getString("recipient"))); + builder.from(ctx.getAddressRepository().getAddress(rs.getString("sender"))); + builder.to(ctx.getAddressRepository().getAddress(rs.getString("recipient"))); builder.sent(rs.getLong("sent")); builder.received(rs.getLong("received")); builder.status(Plaintext.Status.valueOf(rs.getString("status"))); @@ -157,9 +182,11 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito private Collection<Label> findLabels(Connection connection, long messageId) { List<Label> result = new ArrayList<>(); - try { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE id IN (SELECT label_id FROM Message_Label WHERE message_id=" + messageId + ")"); + try ( + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label " + + "WHERE id IN (SELECT label_id FROM Message_Label WHERE message_id=" + messageId + ")") + ) { while (rs.next()) { result.add(getLabel(rs)); } @@ -173,88 +200,96 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito public void save(Plaintext message) { // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = ctx.getAddressRepo().getAddress(message.getFrom().getAddress()); - if (savedAddress == null || savedAddress.getPrivateKey() == null) { - if (savedAddress != null && savedAddress.getAlias() != null) { - message.getFrom().setAlias(savedAddress.getAlias()); - } - ctx.getAddressRepo().save(message.getFrom()); + BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress()); + if (savedAddress == null) { + ctx.getAddressRepository().save(message.getFrom()); + } else if (savedAddress.getPubkey() == null && message.getFrom().getPubkey() != null) { + savedAddress.setPubkey(message.getFrom().getPubkey()); + ctx.getAddressRepository().save(savedAddress); } } try (Connection connection = config.getConnection()) { try { connection.setAutoCommit(false); - // save message - if (message.getId() == null) { - insert(connection, message); - } else { - update(connection, message); - } - - // remove existing labels - Statement stmt = connection.createStatement(); - stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id=" + message.getId()); - - // save labels - PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Label VALUES (" + message.getId() + ", ?)"); - for (Label label : message.getLabels()) { - ps.setLong(1, (Long) label.getId()); - ps.executeUpdate(); - } - + save(connection, message); + updateLabels(connection, message); connection.commit(); } catch (IOException | SQLException e) { - try { - connection.rollback(); - } catch (SQLException e1) { - LOG.debug(e1.getMessage(), e); - } - throw new RuntimeException(e); + connection.rollback(); + throw e; + } + } catch (IOException | SQLException e) { + throw new ApplicationException(e); + } + } + + private void save(Connection connection, Plaintext message) throws IOException, SQLException { + if (message.getId() == null) { + insert(connection, message); + } else { + update(connection, message); + } + } + + private void updateLabels(Connection connection, Plaintext message) throws SQLException { + // remove existing labels + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id=" + message.getId()); + } + // save new labels + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Label VALUES (" + + message.getId() + ", ?)")) { + for (Label label : message.getLabels()) { + ps.setLong(1, (Long) label.getId()); + ps.executeUpdate(); } - } catch (SQLException e) { - throw new RuntimeException(e); } } private void insert(Connection connection, Plaintext message) throws SQLException, IOException { - PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - Statement.RETURN_GENERATED_KEYS); - ps.setBytes(1, message.getInventoryVector() != null ? message.getInventoryVector().getHash() : null); - ps.setString(2, message.getType().name()); - ps.setString(3, message.getFrom().getAddress()); - ps.setString(4, message.getTo() != null ? message.getTo().getAddress() : null); - writeBlob(ps, 5, message); - ps.setLong(6, message.getSent()); - ps.setLong(7, message.getReceived()); - ps.setString(8, message.getStatus() != null ? message.getStatus().name() : null); + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status, initial_hash) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS) + ) { + ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); + ps.setString(2, message.getType().name()); + ps.setString(3, message.getFrom().getAddress()); + ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress()); + writeBlob(ps, 5, message); + ps.setLong(6, message.getSent()); + ps.setLong(7, message.getReceived()); + ps.setString(8, message.getStatus() == null ? null : message.getStatus().name()); + ps.setBytes(9, message.getInitialHash()); - ps.executeUpdate(); - - // get generated id - ResultSet rs = ps.getGeneratedKeys(); - rs.next(); - message.setId(rs.getLong(1)); + ps.executeUpdate(); + // get generated id + try (ResultSet rs = ps.getGeneratedKeys()) { + rs.next(); + message.setId(rs.getLong(1)); + } + } } private void update(Connection connection, Plaintext message) throws SQLException, IOException { - PreparedStatement ps = connection.prepareStatement( - "UPDATE Message SET iv=?, sent=?, received=?, status=? WHERE id=?"); - ps.setBytes(1, message.getInventoryVector() != null ? message.getInventoryVector().getHash() : null); - ps.setLong(2, message.getSent()); - ps.setLong(3, message.getReceived()); - ps.setString(4, message.getStatus() != null ? message.getStatus().name() : null); - ps.setLong(5, (Long) message.getId()); - ps.executeUpdate(); + try (PreparedStatement ps = connection.prepareStatement( + "UPDATE Message SET iv=?, sent=?, received=?, status=?, initial_hash=? WHERE id=?")) { + ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); + ps.setLong(2, message.getSent()); + ps.setLong(3, message.getReceived()); + ps.setString(4, message.getStatus() == null ? null : message.getStatus().name()); + ps.setBytes(5, message.getInitialHash()); + ps.setLong(6, (Long) message.getId()); + ps.executeUpdate(); + } } @Override public void remove(Plaintext message) { try (Connection connection = config.getConnection()) { - try { - connection.setAutoCommit(false); - Statement stmt = connection.createStatement(); + connection.setAutoCommit(false); + try (Statement stmt = connection.createStatement()) { stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id = " + message.getId()); stmt.executeUpdate("DELETE FROM Message WHERE id = " + message.getId()); connection.commit(); diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java new file mode 100644 index 0000000..0fbb2dc --- /dev/null +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java @@ -0,0 +1,103 @@ +package ch.dissem.bitmessage.repository; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.*; +import java.util.LinkedList; +import java.util.List; + +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository { + private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class); + + public JdbcProofOfWorkRepository(JdbcConfig config) { + super(config); + } + + @Override + public Item getItem(byte[] initialHash) { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, " + + "extra_bytes FROM POW WHERE initial_hash=?") + ) { + ps.setBytes(1, initialHash); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + Blob data = rs.getBlob("data"); + return new Item( + Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), + rs.getLong("nonce_trials_per_byte"), + rs.getLong("extra_bytes") + ); + } else { + throw new IllegalArgumentException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); + } + } + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + throw new ApplicationException(e); + } + } + + @Override + public List<byte[]> getItems() { + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT initial_hash FROM POW") + ) { + List<byte[]> result = new LinkedList<>(); + while (rs.next()) { + result.add(rs.getBytes("initial_hash")); + } + return result; + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + throw new ApplicationException(e); + } + } + + @Override + public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, " + + "nonce_trials_per_byte, extra_bytes) VALUES (?, ?, ?, ?, ?)") + ) { + ps.setBytes(1, security().getInitialHash(object)); + writeBlob(ps, 2, object); + ps.setLong(3, object.getVersion()); + ps.setLong(4, nonceTrialsPerByte); + ps.setLong(5, extraBytes); + ps.executeUpdate(); + } catch (IOException | SQLException e) { + LOG.debug("Error storing object of type " + object.getPayload().getClass().getSimpleName(), e); + throw new ApplicationException(e); + } + } + + @Override + public void removeObject(byte[] initialHash) { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("DELETE FROM POW WHERE initial_hash=?") + ) { + ps.setBytes(1, initialHash); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.debug(e.getMessage(), e); + } + } +} diff --git a/repositories/src/main/resources/db/migration/V2.0__Update_table_message.sql b/repositories/src/main/resources/db/migration/V2.0__Update_table_message.sql new file mode 100644 index 0000000..0d81858 --- /dev/null +++ b/repositories/src/main/resources/db/migration/V2.0__Update_table_message.sql @@ -0,0 +1,2 @@ +ALTER TABLE Message ADD COLUMN initial_hash BINARY(64); +ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash); \ No newline at end of file diff --git a/repositories/src/main/resources/db/migration/V2.1__Create_table_POW.sql b/repositories/src/main/resources/db/migration/V2.1__Create_table_POW.sql new file mode 100644 index 0000000..b39c6c5 --- /dev/null +++ b/repositories/src/main/resources/db/migration/V2.1__Create_table_POW.sql @@ -0,0 +1,7 @@ +CREATE TABLE POW ( + initial_hash BINARY(64) PRIMARY KEY, + data BLOB NOT NULL, + version BIGINT NOT NULL, + nonce_trials_per_byte BIGINT NOT NULL, + extra_bytes BIGINT NOT NULL +); diff --git a/repositories/src/main/resources/db/migration/V3.0__Update_table_address.sql b/repositories/src/main/resources/db/migration/V3.0__Update_table_address.sql new file mode 100644 index 0000000..01e036b --- /dev/null +++ b/repositories/src/main/resources/db/migration/V3.0__Update_table_address.sql @@ -0,0 +1 @@ +ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0'; diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java index 18fc83c..bd91329 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java @@ -95,7 +95,7 @@ public class JdbcAddressRepositoryTest extends TestBase { addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh"); List<BitmessageAddress> subscriptions; - + subscriptions = repo.getSubscriptions(5); assertEquals(1, subscriptions.size()); @@ -128,6 +128,35 @@ public class JdbcAddressRepositoryTest extends TestBase { assertEquals("Test-Alias", address.getAlias()); } + @Test + public void ensureExistingKeysAreNotDeleted() { + BitmessageAddress address = new BitmessageAddress(IDENTITY_A); + address.setAlias("Test"); + repo.save(address); + BitmessageAddress identityA = repo.getAddress(IDENTITY_A); + assertNotNull(identityA.getPubkey()); + assertNotNull(identityA.getPrivateKey()); + assertEquals("Test", identityA.getAlias()); + assertFalse(identityA.isChan()); + } + + @Test + public void ensureNewChanIsSavedAndUpdated() { + BitmessageAddress chan = BitmessageAddress.chan(1, "test"); + repo.save(chan); + BitmessageAddress address = repo.getAddress(chan.getAddress()); + assertNotNull(address); + assertTrue(address.isChan()); + + address.setAlias("Test"); + repo.save(address); + + address = repo.getAddress(chan.getAddress()); + assertNotNull(address); + assertTrue(address.isChan()); + assertEquals("Test", address.getAlias()); + } + @Test public void testRemove() throws Exception { BitmessageAddress address = repo.getAddress(IDENTITY_A); diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java index 3dceb24..816eafa 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -33,29 +33,28 @@ import java.util.List; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.utils.Singleton.security; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; public class JdbcMessageRepositoryTest extends TestBase { private BitmessageAddress contactA; private BitmessageAddress contactB; private BitmessageAddress identity; - private TestJdbcConfig config; - private AddressRepository addressRepo; private MessageRepository repo; private Label inbox; private Label drafts; + private Label unread; @Before public void setUp() throws Exception { - config = new TestJdbcConfig(); + TestJdbcConfig config = new TestJdbcConfig(); config.reset(); - addressRepo = new JdbcAddressRepository(config); + AddressRepository addressRepo = new JdbcAddressRepository(config); repo = new JdbcMessageRepository(config); new InternalContext(new BitmessageContext.Builder() - .security(security()) + .cryptography(security()) .addressRepo(addressRepo) .messageRepo(repo) ); @@ -72,27 +71,28 @@ public class JdbcMessageRepositoryTest extends TestBase { inbox = repo.getLabels(Label.Type.INBOX).get(0); drafts = repo.getLabels(Label.Type.DRAFT).get(0); + unread = repo.getLabels(Label.Type.UNREAD).get(0); - addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox); + addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox, unread); addMessage(identity, contactA, Plaintext.Status.DRAFT, drafts); - addMessage(identity, contactB, Plaintext.Status.DRAFT); + addMessage(identity, contactB, Plaintext.Status.DRAFT, unread); } @Test - public void testGetLabels() throws Exception { + public void ensureLabelsAreRetrieved() throws Exception { List<Label> labels = repo.getLabels(); assertEquals(5, labels.size()); } @Test - public void testGetLabelsByType() throws Exception { + public void ensureLabelsCanBeRetrievedByType() throws Exception { List<Label> labels = repo.getLabels(Label.Type.INBOX); assertEquals(1, labels.size()); assertEquals("Inbox", labels.get(0).toString()); } @Test - public void testFindMessagesByLabel() throws Exception { + public void ensureMessagesCanBeFoundByLabel() throws Exception { List<Plaintext> messages = repo.findMessages(inbox); assertEquals(1, messages.size()); Plaintext m = messages.get(0); @@ -101,6 +101,28 @@ public class JdbcMessageRepositoryTest extends TestBase { assertEquals(Plaintext.Status.RECEIVED, m.getStatus()); } + @Test + public void ensureUnreadMessagesCanBeFoundForAllLabels() { + int unread = repo.countUnread(null); + assertThat(unread, is(2)); + } + + @Test + public void ensureUnreadMessagesCanBeFoundByLabel() { + int unread = repo.countUnread(inbox); + assertThat(unread, is(1)); + } + + @Test + public void ensureMessageCanBeRetrievedByInitialHash() { + byte[] initialHash = new byte[64]; + Plaintext message = repo.findMessages(contactA).get(0); + message.setInitialHash(initialHash); + repo.save(message); + Plaintext other = repo.getMessage(initialHash); + assertThat(other, is(message)); + } + @Test public void testFindMessagesByStatus() throws Exception { List<Plaintext> messages = repo.findMessages(Plaintext.Status.RECEIVED); diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java index 044c44c..f56d973 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java @@ -23,6 +23,7 @@ import org.junit.Before; import org.junit.Test; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static ch.dissem.bitmessage.utils.UnixTime.now; @@ -73,7 +74,7 @@ public class JdbcNodeRegistryTest extends TestBase { List<NetworkAddress> knownAddresses = registry.getKnownAddresses(1000, 1); assertEquals(5, knownAddresses.size()); - registry.offerAddresses(Arrays.asList( + registry.offerAddresses(Collections.singletonList( createAddress(1, 8445, 1, now()) )); diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java new file mode 100644 index 0000000..c8fbaf0 --- /dev/null +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.GetPubkey; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author Christian Basler + */ +public class JdbcProofOfWorkRepositoryTest extends TestBase { + private TestJdbcConfig config; + private JdbcProofOfWorkRepository repo; + + @Before + public void setUp() throws Exception { + config = new TestJdbcConfig(); + config.reset(); + + repo = new JdbcProofOfWorkRepository(config); + + repo.putObject(new ObjectMessage.Builder() + .payload(new GetPubkey(new BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(), + 1000, 1000); + } + + @Test + public void ensureObjectIsStored() throws Exception { + int sizeBefore = repo.getItems().size(); + repo.putObject(new ObjectMessage.Builder() + .payload(new GetPubkey(new BitmessageAddress("BM-2D9U2hv3YBMHM1zERP32anKfVKohyPN9x2"))).build(), + 1000, 1000); + assertThat(repo.getItems().size(), is(sizeBefore + 1)); + } + + @Test + public void ensureItemCanBeRetrieved() { + byte[] initialHash = repo.getItems().get(0); + ProofOfWorkRepository.Item item = repo.getItem(initialHash); + assertThat(item, notNullValue()); + assertThat(item.object.getPayload(), instanceOf(GetPubkey.class)); + assertThat(item.nonceTrialsPerByte, is(1000L)); + assertThat(item.extraBytes, is(1000L)); + } + + @Test(expected = RuntimeException.class) + public void ensureRetrievingNonexistingItemThrowsException() { + repo.getItem(new byte[0]); + } + + @Test + public void ensureItemCanBeDeleted() { + byte[] initialHash = repo.getItems().get(0); + repo.removeObject(initialHash); + assertTrue(repo.getItems().isEmpty()); + } + + @Test + public void ensureDeletionOfNonexistingItemIsHandledSilently() { + repo.removeObject(new byte[0]); + } +} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java index c6baaa2..be386cd 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java @@ -18,7 +18,7 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; -import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.utils.Singleton; import static org.mockito.Mockito.mock; @@ -29,7 +29,7 @@ import static org.mockito.Mockito.when; */ public class TestBase { static { - BouncySecurity security = new BouncySecurity(); + BouncyCryptography security = new BouncyCryptography(); Singleton.initialize(security); InternalContext ctx = mock(InternalContext.class); when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); diff --git a/security-bc/src/test/java/ch/dissem/bitmessage/security/SecurityTest.java b/security-bc/src/test/java/ch/dissem/bitmessage/security/SecurityTest.java deleted file mode 100644 index 46a8ae6..0000000 --- a/security-bc/src/test/java/ch/dissem/bitmessage/security/SecurityTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package ch.dissem.bitmessage.security; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.GenericPayload; -import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; -import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.security.bc.BouncySecurity; -import ch.dissem.bitmessage.utils.CallbackWaiter; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.UnixTime; -import org.junit.Test; - -import javax.xml.bind.DatatypeConverter; -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import static ch.dissem.bitmessage.utils.UnixTime.DAY; -import static org.junit.Assert.assertArrayEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Created by chris on 19.07.15. - */ -public class SecurityTest { - public static final byte[] TEST_VALUE = "teststring".getBytes(); - public static final byte[] TEST_SHA1 = DatatypeConverter.parseHexBinary("" - + "b8473b86d4c2072ca9b08bd28e373e8253e865c4"); - public static final byte[] TEST_SHA512 = DatatypeConverter.parseHexBinary("" - + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" - + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72"); - public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" - + "cd566972b5e50104011a92b59fa8e0b1234851ae"); - - private static BouncySecurity security; - - public SecurityTest() { - security = new BouncySecurity(); - Singleton.initialize(security); - InternalContext ctx = mock(InternalContext.class); - when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); - security.setContext(ctx); - } - - @Test - public void testRipemd160() { - assertArrayEquals(TEST_RIPEMD160, security.ripemd160(TEST_VALUE)); - } - - @Test - public void testSha1() { - assertArrayEquals(TEST_SHA1, security.sha1(TEST_VALUE)); - } - - @Test - public void testSha512() { - assertArrayEquals(TEST_SHA512, security.sha512(TEST_VALUE)); - } - - @Test - public void testChaining() { - assertArrayEquals(TEST_SHA512, security.sha512("test".getBytes(), "string".getBytes())); - } - - @Test - public void testDoubleHash() { - assertArrayEquals(security.sha512(TEST_SHA512), security.doubleSha512(TEST_VALUE)); - } - - @Test(expected = IOException.class) - public void testProofOfWorkFails() throws IOException { - ObjectMessage objectMessage = new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * DAY)) // 5 minutes - .objectType(0) - .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) - .build(); - security.checkProofOfWork(objectMessage, 1000, 1000); - } - - @Test - public void testDoProofOfWork() throws Exception { - ObjectMessage objectMessage = new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * DAY)) - .objectType(0) - .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) - .build(); - final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); - security.doProofOfWork(objectMessage, 1000, 1000, - new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] nonce) { - waiter.setValue(nonce); - } - }); - objectMessage.setNonce(waiter.waitForValue()); - security.checkProofOfWork(objectMessage, 1000, 1000); - } -} \ No newline at end of file diff --git a/security-sc/build.gradle b/security-sc/build.gradle deleted file mode 100644 index bfbdcab..0000000 --- a/security-sc/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -uploadArchives { - repositories { - mavenDeployer { - pom.project { - name 'Jabit Spongy Security' - artifactId = 'jabit-security-spongy' - description 'The Security implementation using spongy castle (needed for Android)' - } - } - } -} - -dependencies { - compile project(':domain') - compile 'com.madgag.spongycastle:prov:1.52.0.0' - testCompile 'junit:junit:4.11' -} diff --git a/settings.gradle b/settings.gradle index 2d3e1f6..4caa813 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ rootProject.name = 'Jabit' -include 'domain' +include 'core' include 'networking' @@ -10,6 +10,8 @@ include 'demo' include 'wif' -include 'security-sc' +include 'cryptography-sc' -include 'security-bc' +include 'cryptography-bc' + +include 'extensions' \ No newline at end of file diff --git a/wif/build.gradle b/wif/build.gradle index 5b32a21..93a0248 100644 --- a/wif/build.gradle +++ b/wif/build.gradle @@ -11,9 +11,9 @@ uploadArchives { } dependencies { - compile project(':domain') + compile project(':core') compile 'org.ini4j:ini4j:0.5.4' testCompile 'junit:junit:4.11' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile project(':security-bc') + testCompile project(':cryptography-bc') } diff --git a/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java b/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java index b41d645..ad6098e 100644 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java +++ b/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Base58; import org.ini4j.Ini; import org.ini4j.Profile; @@ -59,6 +60,9 @@ public class WifExporter { section.add("label", identity.getAlias()); section.add("enabled", true); section.add("decoy", false); + if (identity.isChan()) { + section.add("chan", identity.isChan()); + } section.add("noncetrialsperbyte", identity.getPubkey().getNonceTrialsPerByte()); section.add("payloadlengthextrabytes", identity.getPubkey().getExtraBytes()); section.add("privsigningkey", exportSecret(identity.getPrivateKey().getPrivateSigningKey())); @@ -95,7 +99,7 @@ public class WifExporter { try { ini.store(writer); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } return writer.toString(); } diff --git a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java b/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java index e88eaa4..1bd6bcd 100644 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java +++ b/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java @@ -23,8 +23,6 @@ import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Base58; import org.ini4j.Ini; import org.ini4j.Profile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.*; import java.util.Arrays; @@ -39,11 +37,10 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * @author Christian Basler */ public class WifImporter { - private final static Logger LOG = LoggerFactory.getLogger(WifImporter.class); + private static final byte WIF_FIRST_BYTE = (byte) 0x80; + private static final int WIF_SECRET_LENGTH = 37; private final BitmessageContext ctx; - private final Ini ini = new Ini(); - private final List<BitmessageAddress> identities = new LinkedList<>(); public WifImporter(BitmessageContext ctx, File file) throws IOException { @@ -73,6 +70,9 @@ public class WifImporter { section.get("payloadlengthextrabytes", long.class), Pubkey.Feature.bitfield(features) ); + if (section.containsKey("chan")) { + address.setChan(section.get("chan", boolean.class)); + } address.setAlias(section.get("label")); identities.add(address); } @@ -80,10 +80,12 @@ public class WifImporter { private byte[] getSecret(String walletImportFormat) throws IOException { byte[] bytes = Base58.decode(walletImportFormat); - if (bytes[0] != (byte) 0x80) - throw new IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + " was " + bytes[0]); - if (bytes.length != 37) - throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); + if (bytes[0] != WIF_FIRST_BYTE) + throw new IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + + " was " + bytes[0]); + if (bytes.length != WIF_SECRET_LENGTH) + throw new IOException("Unknown format: " + WIF_SECRET_LENGTH + + " bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); byte[] hash = security().doubleSha256(bytes, 33); for (int i = 0; i < 4; i++) { diff --git a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java b/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java index 5ed9025..225f2f9 100644 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java +++ b/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java @@ -17,8 +17,9 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import org.junit.Before; import org.junit.Test; @@ -35,10 +36,11 @@ public class WifExporterTest { @Before public void setUp() throws Exception { ctx = new BitmessageContext.Builder() - .security(new BouncySecurity()) + .cryptography(new BouncyCryptography()) .networkHandler(mock(NetworkHandler.class)) .inventory(mock(Inventory.class)) .messageRepo(mock(MessageRepository.class)) + .powRepo(mock(ProofOfWorkRepository.class)) .nodeRegistry(mock(NodeRegistry.class)) .addressRepo(repo) .build(); @@ -72,16 +74,34 @@ public class WifExporterTest { @Test public void testAddIdentity() throws Exception { - String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]\n" + - "label = Nuked Address\n" + - "enabled = true\n" + - "decoy = false\n" + - "noncetrialsperbyte = 320\n" + - "payloadlengthextrabytes = 14000\n" + - "privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9\n" + - "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck\n\n"; + String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]" + System.lineSeparator() + + "label = Nuked Address" + System.lineSeparator() + + "enabled = true" + System.lineSeparator() + + "decoy = false" + System.lineSeparator() + + "noncetrialsperbyte = 320" + System.lineSeparator() + + "payloadlengthextrabytes = 14000" + System.lineSeparator() + + "privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9" + System.lineSeparator() + + "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck" + System.lineSeparator() + + System.lineSeparator(); importer = new WifImporter(ctx, expected); exporter.addIdentity(importer.getIdentities().get(0)); assertEquals(expected, exporter.toString()); } + + @Test + public void ensureChanIsAdded() throws Exception { + String expected = "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]" + System.lineSeparator() + + "label = general" + System.lineSeparator() + + "enabled = true" + System.lineSeparator() + + "decoy = false" + System.lineSeparator() + + "chan = true" + System.lineSeparator() + + "noncetrialsperbyte = 1000" + System.lineSeparator() + + "payloadlengthextrabytes = 1000" + System.lineSeparator() + + "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ" + System.lineSeparator() + + "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq" + System.lineSeparator() + + System.lineSeparator(); + BitmessageAddress chan = ctx.joinChan("general", "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r"); + exporter.addIdentity(chan); + assertEquals(expected, exporter.toString()); + } } \ No newline at end of file diff --git a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java b/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java index 862b3e3..398ff20 100644 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java +++ b/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java @@ -19,15 +19,13 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.security.bc.BouncySecurity; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import org.junit.Before; import org.junit.Test; import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.any; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class WifImporterTest { @@ -38,10 +36,11 @@ public class WifImporterTest { @Before public void setUp() throws Exception { ctx = new BitmessageContext.Builder() - .security(new BouncySecurity()) + .cryptography(new BouncyCryptography()) .networkHandler(mock(NetworkHandler.class)) .inventory(mock(Inventory.class)) .messageRepo(mock(MessageRepository.class)) + .powRepo(mock(ProofOfWorkRepository.class)) .nodeRegistry(mock(NodeRegistry.class)) .addressRepo(repo) .build(); @@ -68,6 +67,7 @@ public class WifImporterTest { assertNotNull("Private key", identity.getPrivateKey()); assertEquals(32, identity.getPrivateKey().getPrivateEncryptionKey().length); assertEquals(32, identity.getPrivateKey().getPrivateSigningKey().length); + assertFalse(identity.isChan()); } @Test @@ -97,4 +97,20 @@ public class WifImporterTest { importer.importIdentity(identities.get(0)); verify(repo, times(1)).save(identities.get(0)); } + + @Test + public void ensureChanIsImported() throws Exception { + importer = new WifImporter(ctx, "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]\n" + + "label = [chan] general\n" + + "enabled = true\n" + + "decoy = false\n" + + "chan = true\n" + + "noncetrialsperbyte = 1000\n" + + "payloadlengthextrabytes = 1000\n" + + "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ\n" + + "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq\n"); + assertEquals(1, importer.getIdentities().size()); + BitmessageAddress chan = importer.getIdentities().get(0); + assertTrue(chan.isChan()); + } } \ No newline at end of file