diff --git a/.travis.yml b/.travis.yml index a98b760..9bcf999 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,3 @@ language: java - +jdk: + - oraclejdk8 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..fb45cc6 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,21 @@ Setup Add Jabit as Gradle dependency: ```Gradle -compile 'ch.dissem.jabit:jabit-domain:0.2.0' +compile 'ch.dissem.jabit:jabit-core:0.2.0' ``` Unless you want to implement your own, also add the following: ```Gradle compile 'ch.dissem.jabit:jabit-networking:0.2.0' compile 'ch.dissem.jabit:jabit-repositories:0.2.0' +compile 'ch.dissem.jabit:jabit-cryptography-bc:0.2.0' ``` And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add: ```Gradle compile 'ch.dissem.jabit:jabit-wif:0.2.0' ``` +For Android clients use `jabit-cryptography-sc` instead of `jabit-cryptography-bc`. + Usage ----- @@ -53,6 +56,7 @@ BitmessageContext ctx = new BitmessageContext.Builder() .messageRepo(new JdbcMessageRepository(jdbcConfig)) .nodeRegistry(new MemoryNodeRegistry()) .networkHandler(new NetworkNode()) + .cryptography(new BouncyCryptography()) .build(); ``` This creates a simple context using a H2 database that will be created in the user's home directory. Next you'll need to diff --git a/build.gradle b/build.gradle index 7bf5c3f..dc918be 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ subprojects { sourceCompatibility = 1.7 group = 'ch.dissem.jabit' - version = '0.2.0' + version = '1.0.0' ext.isReleaseVersion = !version.endsWith("SNAPSHOT") @@ -13,6 +13,12 @@ subprojects { mavenCentral() } + test { + testLogging { + exceptionFormat = 'full' + } + } + task javadocJar(type: Jar) { classifier = 'javadoc' from javadoc @@ -37,11 +43,6 @@ subprojects { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - if (!hasProperty('ossrhUsername')) { - ext.ossrhUsername = 'dummy' - ext.ossrhPassword = 'dummy' - } - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { authentication(userName: ossrhUsername, password: ossrhPassword) } diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..8754cb3 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,31 @@ +uploadArchives { + repositories { + mavenDeployer { + pom.project { + name 'Jabit Core' + artifactId = 'jabit-core' + description 'A Java implementation of the Bitmessage protocol. This is the core part. You\'ll either need the networking and repositories modules, too, or implement your own.' + } + } + } +} + +configurations { + testArtifacts.extendsFrom testRuntime +} + +task testJar(type: Jar) { + classifier = 'test' + from sourceSets.test.output +} + +artifacts { + testArtifacts testJar +} + +dependencies { + compile 'org.slf4j:slf4j-api:1.7.12' + testCompile 'junit:junit:4.11' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile project(':cryptography-bc') +} diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java new file mode 100644 index 0000000..28e8483 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -0,0 +1,515 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.payload.*; +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.utils.Property; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.*; + +import static ch.dissem.bitmessage.entity.Plaintext.Status.*; +import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; +import static ch.dissem.bitmessage.utils.UnixTime.HOUR; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; + +/** + *

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

+ * You'll need the Builder to create a BitmessageContext, and set the following properties: + *
    + *
  • addressRepo
  • + *
  • inventory
  • + *
  • nodeRegistry
  • + *
  • networkHandler
  • + *
  • messageRepo
  • + *
  • streams
  • + *
+ *

The default implementations in the different module builds can be used.

+ *

The port defaults to 8444 (the default Bitmessage port)

+ */ +public class BitmessageContext { + public static final int CURRENT_VERSION = 3; + private final static Logger LOG = LoggerFactory.getLogger(BitmessageContext.class); + + private final ExecutorService pool; + + private final InternalContext ctx; + + private final Listener listener; + private final NetworkHandler.MessageListener networkListener; + + private final boolean sendPubkeyOnIdentityCreation; + + private BitmessageContext(Builder builder) { + ctx = new InternalContext(builder); + listener = builder.listener; + networkListener = new DefaultMessageListener(ctx, listener); + + // As this thread is used for parts that do POW, which itself uses parallel threads, only + // one should be executed at any time. + pool = Executors.newFixedThreadPool(1); + + sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation; + + new Timer().schedule(new TimerTask() { + @Override + public void run() { + ctx.getProofOfWorkService().doMissingProofOfWork(); + } + }, 30_000); // After 30 seconds + } + + public AddressRepository addresses() { + return ctx.getAddressRepository(); + } + + public MessageRepository messages() { + return ctx.getMessageRepository(); + } + + public BitmessageAddress createIdentity(boolean shorter, Feature... features) { + final BitmessageAddress identity = new BitmessageAddress(new PrivateKey( + shorter, + ctx.getStreams()[0], + ctx.getNetworkNonceTrialsPerByte(), + ctx.getNetworkExtraBytes(), + features + )); + ctx.getAddressRepository().save(identity); + if (sendPubkeyOnIdentityCreation) { + pool.submit(new Runnable() { + @Override + public void run() { + ctx.sendPubkey(identity, identity.getStream()); + } + }); + } + return identity; + } + + public void addDistributedMailingList(String address, String alias) { + // TODO + throw new RuntimeException("not implemented"); + } + + public void broadcast(final BitmessageAddress from, final String subject, final String message) { + pool.submit(new Runnable() { + @Override + public void run() { + Plaintext msg = new Plaintext.Builder(BROADCAST) + .from(from) + .message(subject, message) + .build(); + + LOG.info("Sending message."); + msg.setStatus(DOING_PROOF_OF_WORK); + ctx.getMessageRepository().save(msg); + ctx.send( + from, + from, + Factory.getBroadcast(from, msg), + +2 * DAY + ); + msg.setStatus(SENT); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.BROADCAST, Label.Type.SENT)); + ctx.getMessageRepository().save(msg); + } + }); + } + + public void send(final BitmessageAddress from, final BitmessageAddress to, final String subject, final String message) { + if (from.getPrivateKey() == null) { + throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); + } + pool.submit(new Runnable() { + @Override + public void run() { + Plaintext msg = new Plaintext.Builder(MSG) + .from(from) + .to(to) + .message(subject, message) + .labels(messages().getLabels(Label.Type.SENT)) + .build(); + if (to.getPubkey() == null) { + tryToFindMatchingPubkey(to); + } + if (to.getPubkey() == null) { + LOG.info("Public key is missing from recipient. Requesting."); + requestPubkey(from, to); + msg.setStatus(PUBKEY_REQUESTED); + ctx.getMessageRepository().save(msg); + } else { + LOG.info("Sending message."); + msg.setStatus(DOING_PROOF_OF_WORK); + ctx.getMessageRepository().save(msg); + ctx.send( + from, + to, + new Msg(msg), + +2 * DAY + ); + msg.setStatus(SENT); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); + ctx.getMessageRepository().save(msg); + } + } + }); + } + + public void send(final Plaintext msg) { + if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) { + throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); + } + pool.submit(new Runnable() { + @Override + public void run() { + BitmessageAddress to = msg.getTo(); + if (to.getPubkey() == null) { + tryToFindMatchingPubkey(to); + } + if (to.getPubkey() == null) { + LOG.info("Public key is missing from recipient. Requesting."); + requestPubkey(msg.getFrom(), to); + msg.setStatus(PUBKEY_REQUESTED); + ctx.getMessageRepository().save(msg); + } else { + LOG.info("Sending message."); + msg.setStatus(DOING_PROOF_OF_WORK); + ctx.getMessageRepository().save(msg); + ctx.send( + msg.getFrom(), + to, + new Msg(msg), + +2 * DAY + ); + msg.setStatus(SENT); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); + ctx.getMessageRepository().save(msg); + } + } + }); + } + + private void requestPubkey(BitmessageAddress requestingIdentity, BitmessageAddress address) { + ctx.send( + requestingIdentity, + address, + new GetPubkey(address), + +28 * DAY + ); + } + + public void startup() { + ctx.getNetworkHandler().start(networkListener); + } + + public void shutdown() { + ctx.getNetworkHandler().stop(); + } + + /** + * @param host a trusted node that must be reliable (it's used for every synchronization) + * @param port of the trusted host, default is 8444 + * @param timeoutInSeconds synchronization should end no later than about 5 seconds after the timeout elapsed, even + * if not all objects were fetched + * @param wait waits for the synchronization thread to finish + */ + public void synchronize(InetAddress host, int port, long timeoutInSeconds, boolean wait) { + Future future = ctx.getNetworkHandler().synchronize(host, port, networkListener, timeoutInSeconds); + if (wait) { + try { + future.get(); + } catch (InterruptedException e) { + LOG.info("Thread was interrupted. Trying to shut down synchronization and returning."); + future.cancel(true); + } catch (CancellationException | ExecutionException e) { + LOG.debug(e.getMessage(), e); + } + } + } + + /** + * Send a custom message to a specific node (that should implement handling for this message type) and returns + * the response, which in turn is expected to be a {@link CustomMessage}. + * + * @param server the node's address + * @param port the node's port + * @param request the request + * @return the response + */ + public CustomMessage send(InetAddress server, int port, CustomMessage request) { + return ctx.getNetworkHandler().send(server, port, request); + } + + public void cleanup() { + ctx.getInventory().cleanup(); + } + + public boolean isRunning() { + return ctx.getNetworkHandler().isRunning(); + } + + public void addContact(BitmessageAddress contact) { + ctx.getAddressRepository().save(contact); + tryToFindMatchingPubkey(contact); + if (contact.getPubkey() == null) { + ctx.requestPubkey(contact); + } + } + + private void tryToFindMatchingPubkey(BitmessageAddress address) { + for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) { + try { + Pubkey pubkey = (Pubkey) object.getPayload(); + if (address.getVersion() == 4) { + V4Pubkey v4Pubkey = (V4Pubkey) pubkey; + if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) { + v4Pubkey.decrypt(address.getPublicDecryptionKey()); + if (object.isSignatureValid(v4Pubkey)) { + address.setPubkey(v4Pubkey); + ctx.getAddressRepository().save(address); + break; + } else { + LOG.info("Found pubkey for " + address + " but signature is invalid"); + } + } + } else { + if (Arrays.equals(pubkey.getRipe(), address.getRipe())) { + address.setPubkey(pubkey); + ctx.getAddressRepository().save(address); + break; + } + } + } catch (Exception e) { + LOG.debug(e.getMessage(), e); + } + } + } + + public void addSubscribtion(BitmessageAddress address) { + address.setSubscribed(true); + ctx.getAddressRepository().save(address); + tryToFindBroadcastsForAddress(address); + } + + private void tryToFindBroadcastsForAddress(BitmessageAddress address) { + for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), Broadcast.getVersion(address), ObjectType.BROADCAST)) { + try { + Broadcast broadcast = (Broadcast) object.getPayload(); + broadcast.decrypt(address); + listener.receive(broadcast.getPlaintext()); + } catch (DecryptionFailedException ignore) { + } catch (Exception e) { + LOG.debug(e.getMessage(), e); + } + } + } + + public Property status() { + return new Property("status", null, + ctx.getNetworkHandler().getNetworkStatus() + ); + } + + /** + * Returns the {@link InternalContext} - normally you wouldn't need it, + * unless you are doing something crazy with the protocol. + */ + public InternalContext internals() { + return ctx; + } + + public interface Listener { + void receive(Plaintext plaintext); + } + + public static final class Builder { + int port = 8444; + Inventory inventory; + NodeRegistry nodeRegistry; + NetworkHandler networkHandler; + AddressRepository addressRepo; + MessageRepository messageRepo; + ProofOfWorkRepository proofOfWorkRepository; + ProofOfWorkEngine proofOfWorkEngine; + Cryptography cryptography; + MessageCallback messageCallback; + CustomCommandHandler customCommandHandler; + Listener listener; + int connectionLimit = 150; + long connectionTTL = 30 * MINUTE; + boolean sendPubkeyOnIdentityCreation = true; + long pubkeyTTL = 28; + + public Builder() { + } + + public Builder port(int port) { + this.port = port; + return this; + } + + public Builder inventory(Inventory inventory) { + this.inventory = inventory; + return this; + } + + public Builder nodeRegistry(NodeRegistry nodeRegistry) { + this.nodeRegistry = nodeRegistry; + return this; + } + + public Builder networkHandler(NetworkHandler networkHandler) { + this.networkHandler = networkHandler; + return this; + } + + public Builder addressRepo(AddressRepository addressRepo) { + this.addressRepo = addressRepo; + return this; + } + + public Builder messageRepo(MessageRepository messageRepo) { + this.messageRepo = messageRepo; + return this; + } + + public Builder powRepo(ProofOfWorkRepository proofOfWorkRepository) { + this.proofOfWorkRepository = proofOfWorkRepository; + return this; + } + + public Builder cryptography(Cryptography cryptography) { + this.cryptography = cryptography; + return this; + } + + public Builder messageCallback(MessageCallback callback) { + this.messageCallback = callback; + return this; + } + + public Builder customCommandHandler(CustomCommandHandler handler) { + this.customCommandHandler = handler; + return this; + } + + public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) { + this.proofOfWorkEngine = proofOfWorkEngine; + return this; + } + + public Builder listener(Listener listener) { + this.listener = listener; + return this; + } + + public Builder connectionLimit(int connectionLimit) { + this.connectionLimit = connectionLimit; + return this; + } + + public Builder connectionTTL(int hours) { + this.connectionTTL = hours * HOUR; + return this; + } + + /** + * By default a client will send the public key when an identity is being created. On weaker devices + * this behaviour might not be desirable. + */ + public Builder doNotSendPubkeyOnIdentityCreation() { + this.sendPubkeyOnIdentityCreation = false; + return this; + } + + /** + * Time to live in seconds for public keys the client sends. Defaults to the maximum of 28 days, + * but on weak devices smaller values might be desirable. + *

+ * 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"); + this.pubkeyTTL = days; + return this; + } + + public BitmessageContext build() { + nonNull("inventory", inventory); + nonNull("nodeRegistry", nodeRegistry); + nonNull("networkHandler", networkHandler); + nonNull("addressRepo", addressRepo); + nonNull("messageRepo", messageRepo); + nonNull("proofOfWorkRepo", proofOfWorkRepository); + if (proofOfWorkEngine == null) { + proofOfWorkEngine = new MultiThreadedPOWEngine(); + } + if (messageCallback == null) { + messageCallback = new MessageCallback() { + @Override + public void proofOfWorkStarted(ObjectPayload message) { + } + + @Override + public void proofOfWorkCompleted(ObjectPayload message) { + } + + @Override + public void messageOffered(ObjectPayload message, InventoryVector iv) { + } + + @Override + public void messageAcknowledged(InventoryVector iv) { + } + }; + } + if (customCommandHandler == null) { + customCommandHandler = new CustomCommandHandler() { + @Override + public MessagePayload handle(CustomMessage request) { + throw new RuntimeException("Received custom request, but no custom command handler configured."); + } + }; + } + return new BitmessageContext(this); + } + + private void nonNull(String name, Object o) { + if (o == null) throw new IllegalStateException(name + " must not be null"); + } + } + +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java similarity index 76% rename from domain/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java rename to core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java index 105cc96..b2366e4 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java +++ b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java @@ -69,9 +69,10 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { } protected void receive(ObjectMessage object, GetPubkey getPubkey) { - BitmessageAddress identity = ctx.getAddressRepo().findIdentity(getPubkey.getRipeTag()); + BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag()); if (identity != null && identity.getPrivateKey() != null) { - LOG.debug("Got pubkey request for identity " + identity); + LOG.info("Got pubkey request for identity " + identity); + // FIXME: only send pubkey if it wasn't sent in the last 28 days ctx.sendPubkey(identity, object.getStream()); } } @@ -81,40 +82,42 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { try { if (pubkey instanceof V4Pubkey) { V4Pubkey v4Pubkey = (V4Pubkey) pubkey; - address = ctx.getAddressRepo().findContact(v4Pubkey.getTag()); + address = ctx.getAddressRepository().findContact(v4Pubkey.getTag()); if (address != null) { v4Pubkey.decrypt(address.getPublicDecryptionKey()); } } else { - address = ctx.getAddressRepo().findContact(pubkey.getRipe()); + address = ctx.getAddressRepository().findContact(pubkey.getRipe()); } if (address != null) { - address.setPubkey(pubkey); - LOG.debug("Got pubkey for contact " + address); - ctx.getAddressRepo().save(address); - List messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address); - LOG.debug("Sending " + messages.size() + " messages for contact " + address); - for (Plaintext msg : messages) { - msg.setStatus(DOING_PROOF_OF_WORK); - ctx.getMessageRepository().save(msg); - ctx.send( - msg.getFrom(), - msg.getTo(), - new Msg(msg), - +2 * DAY, - ctx.getNonceTrialsPerByte(msg.getTo()), - ctx.getExtraBytes(msg.getTo()) - ); - msg.setStatus(SENT); - ctx.getMessageRepository().save(msg); - } + updatePubkey(address, pubkey); } } catch (DecryptionFailedException ignore) { } } + private void updatePubkey(BitmessageAddress address, Pubkey pubkey){ + address.setPubkey(pubkey); + LOG.info("Got pubkey for contact " + address); + ctx.getAddressRepository().save(address); + List<Plaintext> messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address); + LOG.info("Sending " + messages.size() + " messages for contact " + address); + for (Plaintext msg : messages) { + msg.setStatus(DOING_PROOF_OF_WORK); + ctx.getMessageRepository().save(msg); + ctx.send( + msg.getFrom(), + msg.getTo(), + new Msg(msg), + +2 * DAY + ); + msg.setStatus(SENT); + ctx.getMessageRepository().save(msg); + } + } + protected void receive(ObjectMessage object, Msg msg) throws IOException { - for (BitmessageAddress identity : ctx.getAddressRepo().getIdentities()) { + for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) { try { msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey()); msg.getPlaintext().setTo(identity); @@ -126,6 +129,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { msg.getPlaintext().setInventoryVector(object.getInventoryVector()); ctx.getMessageRepository().save(msg.getPlaintext()); listener.receive(msg.getPlaintext()); + updatePubkey(msg.getPlaintext().getFrom(), msg.getPlaintext().getFrom().getPubkey()); } break; } catch (DecryptionFailedException ignore) { @@ -135,7 +139,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException { byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null; - for (BitmessageAddress subscription : ctx.getAddressRepo().getSubscriptions(broadcast.getVersion())) { + for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) { if (tag != null && !Arrays.equals(tag, subscription.getTag())) { continue; } @@ -149,6 +153,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { broadcast.getPlaintext().setInventoryVector(object.getInventoryVector()); ctx.getMessageRepository().save(broadcast.getPlaintext()); listener.receive(broadcast.getPlaintext()); + updatePubkey(broadcast.getPlaintext().getFrom(), broadcast.getPlaintext().getFrom().getPubkey()); } } catch (DecryptionFailedException ignore) { } diff --git a/domain/src/main/java/ch/dissem/bitmessage/InternalContext.java b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java similarity index 55% rename from domain/src/main/java/ch/dissem/bitmessage/InternalContext.java rename to core/src/main/java/ch/dissem/bitmessage/InternalContext.java index 8ac64e0..9971b9b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/InternalContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java @@ -16,12 +16,14 @@ package ch.dissem.bitmessage; -import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Encrypted; +import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.Broadcast; import ch.dissem.bitmessage.entity.payload.GetPubkey; import ch.dissem.bitmessage.entity.payload.ObjectPayload; import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.utils.Security; +import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +31,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.TreeSet; -import static ch.dissem.bitmessage.utils.UnixTime.DAY; - /** * The internal context should normally only be used for port implementations. If you need it in your client * implementation, you're either doing something wrong, something very weird, or the BitmessageContext should @@ -42,32 +42,64 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; public class InternalContext { private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class); + private final Cryptography cryptography; private final Inventory inventory; private final NodeRegistry nodeRegistry; private final NetworkHandler networkHandler; private final AddressRepository addressRepository; private final MessageRepository messageRepository; + private final ProofOfWorkRepository proofOfWorkRepository; private final ProofOfWorkEngine proofOfWorkEngine; + private final MessageCallback messageCallback; + private final CustomCommandHandler customCommandHandler; + private final ProofOfWorkService proofOfWorkService; private final TreeSet<Long> streams = new TreeSet<>(); private final int port; - private long networkNonceTrialsPerByte = 1000; - private long networkExtraBytes = 1000; - private long clientNonce; + private final long clientNonce; + private final long networkNonceTrialsPerByte = 1000; + private final long networkExtraBytes = 1000; + private final long pubkeyTTL; + private long connectionTTL; + private int connectionLimit; public InternalContext(BitmessageContext.Builder builder) { + this.cryptography = builder.cryptography; this.inventory = builder.inventory; this.nodeRegistry = builder.nodeRegistry; this.networkHandler = builder.networkHandler; this.addressRepository = builder.addressRepo; this.messageRepository = builder.messageRepo; + this.proofOfWorkRepository = builder.proofOfWorkRepository; + this.proofOfWorkService = new ProofOfWorkService(); this.proofOfWorkEngine = builder.proofOfWorkEngine; - this.clientNonce = Security.randomNonce(); + this.clientNonce = cryptography.randomNonce(); + this.messageCallback = builder.messageCallback; + this.customCommandHandler = builder.customCommandHandler; + this.port = builder.port; + this.connectionLimit = builder.connectionLimit; + this.connectionTTL = builder.connectionTTL; + this.pubkeyTTL = builder.pubkeyTTL; - port = builder.port; - streams.add(1L); // FIXME + Singleton.initialize(cryptography); - init(inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, proofOfWorkEngine); + // TODO: streams of new identities and subscriptions should also be added. This works only after a restart. + for (BitmessageAddress address : addressRepository.getIdentities()) { + streams.add(address.getStream()); + } + for (BitmessageAddress address : addressRepository.getSubscriptions()) { + streams.add(address.getStream()); + } + if (streams.isEmpty()) { + streams.add(1L); + } + + init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, + proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, + messageCallback, customCommandHandler); + for (BitmessageAddress identity : addressRepository.getIdentities()) { + streams.add(identity.getStream()); + } } private void init(Object... objects) { @@ -78,6 +110,10 @@ public class InternalContext { } } + public Cryptography getCryptography() { + return cryptography; + } + public Inventory getInventory() { return inventory; } @@ -90,7 +126,7 @@ public class InternalContext { return networkHandler; } - public AddressRepository getAddressRepo() { + public AddressRepository getAddressRepository() { return addressRepository; } @@ -98,10 +134,18 @@ public class InternalContext { return messageRepository; } + public ProofOfWorkRepository getProofOfWorkRepository() { + return proofOfWorkRepository; + } + public ProofOfWorkEngine getProofOfWorkEngine() { return proofOfWorkEngine; } + public ProofOfWorkService getProofOfWorkService() { + return proofOfWorkService; + } + public long[] getStreams() { long[] result = new long[streams.size()]; int i = 0; @@ -111,14 +155,6 @@ public class InternalContext { return result; } - public void addStream(long stream) { - streams.add(stream); - } - - public void removeStream(long stream) { - streams.remove(stream); - } - public int getPort() { return port; } @@ -127,26 +163,17 @@ public class InternalContext { return networkNonceTrialsPerByte; } - public long getNonceTrialsPerByte(BitmessageAddress address) { - long nonceTrialsPerByte = address.getPubkey().getNonceTrialsPerByte(); - return networkNonceTrialsPerByte > nonceTrialsPerByte ? networkNonceTrialsPerByte : nonceTrialsPerByte; - } - public long getNetworkExtraBytes() { return networkExtraBytes; } - public long getExtraBytes(BitmessageAddress address) { - long extraBytes = address.getPubkey().getExtraBytes(); - return networkExtraBytes > extraBytes ? networkExtraBytes : extraBytes; - } - - public void send(BitmessageAddress from, BitmessageAddress to, ObjectPayload payload, long timeToLive, long nonceTrialsPerByte, long extraBytes) { + public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload, + final long timeToLive) { try { if (to == null) to = from; long expires = UnixTime.now(+timeToLive); LOG.info("Expires at " + expires); - ObjectMessage object = new ObjectMessage.Builder() + final ObjectMessage object = new ObjectMessage.Builder() .stream(to.getStream()) .expiresTime(expires) .payload(payload) @@ -159,64 +186,58 @@ public class InternalContext { } else if (payload instanceof Encrypted) { object.encrypt(to.getPubkey()); } - Security.doProofOfWork(object, proofOfWorkEngine, nonceTrialsPerByte, extraBytes); - if (payload instanceof PlaintextHolder) { - Plaintext plaintext = ((PlaintextHolder) payload).getPlaintext(); - plaintext.setInventoryVector(object.getInventoryVector()); - messageRepository.save(plaintext); - } - inventory.storeObject(object); - networkHandler.offer(object.getInventoryVector()); + messageCallback.proofOfWorkStarted(payload); + proofOfWorkService.doProofOfWork(to, object); } catch (IOException e) { throw new RuntimeException(e); } } - public void sendPubkey(BitmessageAddress identity, long targetStream) { + public void sendPubkey(final BitmessageAddress identity, final long targetStream) { try { - long expires = UnixTime.now(+28 * DAY); + long expires = UnixTime.now(pubkeyTTL); LOG.info("Expires at " + expires); - ObjectMessage response = new ObjectMessage.Builder() + final ObjectMessage response = new ObjectMessage.Builder() .stream(targetStream) .expiresTime(expires) .payload(identity.getPubkey()) .build(); response.sign(identity.getPrivateKey()); - response.encrypt(Security.createPublicKey(identity.getPublicDecryptionKey()).getEncoded(false)); - Security.doProofOfWork(response, proofOfWorkEngine, networkNonceTrialsPerByte, networkExtraBytes); - if (response.isSigned()) { - response.sign(identity.getPrivateKey()); - } - if (response instanceof Encrypted) { - response.encrypt(Security.createPublicKey(identity.getPublicDecryptionKey()).getEncoded(false)); - } - inventory.storeObject(response); - networkHandler.offer(response.getInventoryVector()); - // TODO: save that the pubkey was just sent, and on which stream! + response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey())); + messageCallback.proofOfWorkStarted(identity.getPubkey()); + // TODO: remember that the pubkey is just about to be sent, and on which stream! + proofOfWorkService.doProofOfWork(response); } catch (IOException e) { throw new RuntimeException(e); } } - public void requestPubkey(BitmessageAddress contact) { - long expires = UnixTime.now(+2 * DAY); + public void requestPubkey(final BitmessageAddress contact) { + long expires = UnixTime.now(+pubkeyTTL); LOG.info("Expires at " + expires); - ObjectMessage response = new ObjectMessage.Builder() + final ObjectMessage response = new ObjectMessage.Builder() .stream(contact.getStream()) .expiresTime(expires) .payload(new GetPubkey(contact)) .build(); - Security.doProofOfWork(response, proofOfWorkEngine, networkNonceTrialsPerByte, networkExtraBytes); - inventory.storeObject(response); - networkHandler.offer(response.getInventoryVector()); + messageCallback.proofOfWorkStarted(response.getPayload()); + proofOfWorkService.doProofOfWork(response); } public long getClientNonce() { return clientNonce; } - public void setClientNonce(long clientNonce) { - this.clientNonce = clientNonce; + public long getConnectionTTL() { + return connectionTTL; + } + + public int getConnectionLimit() { + return connectionLimit; + } + + public CustomCommandHandler getCustomCommandHandler() { + return customCommandHandler; } public interface ContextHolder { diff --git a/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java b/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java new file mode 100644 index 0000000..d09ff97 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; + +/** + * Callback for message sending events, mostly so the user can be notified when POW is done. + */ +public interface MessageCallback { + /** + * Called before calculation of proof of work begins. + */ + void proofOfWorkStarted(ObjectPayload message); + + /** + * Called after calculation of proof of work finished. + */ + void proofOfWorkCompleted(ObjectPayload message); + + /** + * Called once the message is offered to the network. Please note that this doesn't mean the message was sent, + * if the client is not connected to the network it's just stored in the inventory. + * <p> + * Also, please note that this is where the original payload as well as the {@link InventoryVector} of the sent + * message is available. If the callback needs the IV for some reason, it should be retrieved here. (Plaintext + * and Broadcast messages will have their IV property set automatically though.) + * </p> + */ + void messageOffered(ObjectPayload message, InventoryVector iv); + + /** + * This isn't called yet, as ACK messages aren't being processed yet. Also, this is only relevant for Plaintext + * messages. + */ + void messageAcknowledged(InventoryVector iv); +} 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..19e231f --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java @@ -0,0 +1,82 @@ +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.PlaintextHolder; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.ports.Cryptography; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalContext.ContextHolder { + private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class); + + private Cryptography cryptography; + private InternalContext ctx; + private ProofOfWorkRepository powRepo; + private MessageRepository messageRepo; + + public void doMissingProofOfWork() { + List<byte[]> items = powRepo.getItems(); + if (items.isEmpty()) return; + + LOG.info("Doing POW for " + items.size() + " tasks."); + for (byte[] initialHash : items) { + ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); + cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, this); + } + } + + public void doProofOfWork(ObjectMessage object) { + doProofOfWork(null, object); + } + + public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) { + long nonceTrialsPerByte = recipient == null ? + ctx.getNetworkNonceTrialsPerByte() : recipient.getPubkey().getNonceTrialsPerByte(); + long extraBytes = recipient == null ? + ctx.getNetworkExtraBytes() : recipient.getPubkey().getExtraBytes(); + + powRepo.putObject(object, nonceTrialsPerByte, extraBytes); + if (object.getPayload() instanceof PlaintextHolder) { + Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext(); + plaintext.setInitialHash(cryptography.getInitialHash(object)); + messageRepo.save(plaintext); + } + cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this); + } + + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + ObjectMessage object = powRepo.getItem(initialHash).object; + object.setNonce(nonce); +// messageCallback.proofOfWorkCompleted(payload); + Plaintext plaintext = messageRepo.getMessage(initialHash); + if (plaintext != null) { + plaintext.setInventoryVector(object.getInventoryVector()); + messageRepo.save(plaintext); + } + ctx.getInventory().storeObject(object); + ctx.getProofOfWorkRepository().removeObject(initialHash); + ctx.getNetworkHandler().offer(object.getInventoryVector()); +// messageCallback.messageOffered(payload, object.getInventoryVector()); + } + + @Override + public void setContext(InternalContext ctx) { + this.ctx = ctx; + this.cryptography = security(); + this.powRepo = ctx.getProofOfWorkRepository(); + this.messageRepo = ctx.getMessageRepository(); + } +} 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Addr.java 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 86% 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 eb348a2..931776c 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java @@ -19,22 +19,27 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.utils.*; +import ch.dissem.bitmessage.utils.AccessCounter; +import ch.dissem.bitmessage.utils.Base58; +import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.Encode; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.Serializable; import java.util.Arrays; import java.util.Objects; import static ch.dissem.bitmessage.utils.Decode.bytes; import static ch.dissem.bitmessage.utils.Decode.varInt; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address * holding private keys. */ -public class BitmessageAddress { +public class BitmessageAddress implements Serializable { private final long version; private final long stream; private final byte[] ripe; @@ -62,19 +67,19 @@ public class BitmessageAddress { Encode.varInt(version, os); Encode.varInt(stream, os); if (version < 4) { - byte[] checksum = Security.sha512(os.toByteArray(), ripe); + byte[] checksum = security().sha512(os.toByteArray(), ripe); this.tag = null; this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } else { // for tag and decryption key, the checksum has to be created with 0x00 padding - byte[] checksum = Security.doubleSha512(os.toByteArray(), ripe); + byte[] checksum = security().doubleSha512(os.toByteArray(), ripe); this.tag = Arrays.copyOfRange(checksum, 32, 64); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } // but for the address and its checksum they need to be stripped int offset = Bytes.numberOfLeadingZeros(ripe); os.write(ripe, offset, ripe.length - offset); - byte[] checksum = Security.doubleSha512(os.toByteArray()); + byte[] checksum = security().doubleSha512(os.toByteArray()); os.write(checksum, 0, 4); this.address = "BM-" + Base58.encode(os.toByteArray()); } catch (IOException e) { @@ -82,7 +87,7 @@ public class BitmessageAddress { } } - BitmessageAddress(Pubkey publicKey) { + public BitmessageAddress(Pubkey publicKey) { this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe()); this.pubkey = publicKey; } @@ -103,18 +108,18 @@ public class BitmessageAddress { this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20); // test checksum - byte[] checksum = Security.doubleSha512(bytes, bytes.length - 4); + byte[] checksum = security().doubleSha512(bytes, bytes.length - 4); byte[] expectedChecksum = bytes(in, 4); for (int i = 0; i < 4; i++) { if (expectedChecksum[i] != checksum[i]) throw new IllegalArgumentException("Checksum of address failed"); } if (version < 4) { - checksum = Security.sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); + checksum = security().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); this.tag = null; this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } else { - checksum = Security.doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); + checksum = security().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); this.tag = Arrays.copyOfRange(checksum, 32, 64); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } @@ -129,7 +134,7 @@ public class BitmessageAddress { Encode.varInt(version, out); Encode.varInt(stream, out); out.write(ripe); - return Arrays.copyOfRange(Security.doubleSha512(out.toByteArray()), 32, 64); + return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64); } catch (IOException e) { throw new RuntimeException(e); } 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..5702b6e --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.utils.AccessCounter; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.*; + +import static ch.dissem.bitmessage.utils.Decode.bytes; +import static ch.dissem.bitmessage.utils.Decode.varString; + +/** + * @author Christian Basler + */ +public class CustomMessage implements MessagePayload { + public static final String COMMAND_ERROR = "ERROR"; + + private final String command; + private final byte[] data; + + public CustomMessage(String command) { + this.command = command; + this.data = null; + } + + public CustomMessage(String command, byte[] data) { + this.command = command; + this.data = data; + } + + public static CustomMessage read(InputStream in, int length) throws IOException { + AccessCounter counter = new AccessCounter(); + return new CustomMessage(varString(in, counter), bytes(in, length - counter.length())); + } + + @Override + public Command getCommand() { + return Command.CUSTOM; + } + + public String getCustomCommand() { + return command; + } + + public byte[] getData() { + if (data != null) { + return data; + } else { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void write(OutputStream out) throws IOException { + if (data != null) { + Encode.varString(command, out); + out.write(data); + } else { + throw new RuntimeException("Tried to write custom message without data. " + + "Programmer: did you forget to override #write()?"); + } + } + + public boolean isError() { + return COMMAND_ERROR.equals(command); + } + + public static CustomMessage error(String message) { + try { + return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } +} 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java 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 92% 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..e272bbc 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java @@ -44,10 +44,10 @@ public class GetData implements MessagePayload { } @Override - public void write(OutputStream stream) throws IOException { - Encode.varInt(inventory.size(), stream); + public void write(OutputStream out) throws IOException { + Encode.varInt(inventory.size(), out); for (InventoryVector iv : inventory) { - iv.write(stream); + iv.write(out); } } 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 93% 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..0135ec0 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java @@ -44,10 +44,10 @@ public class Inv implements MessagePayload { } @Override - public void write(OutputStream stream) throws IOException { - Encode.varInt(inventory.size(), stream); + public void write(OutputStream out) throws IOException { + Encode.varInt(inventory.size(), out); for (InventoryVector iv : inventory) { - iv.write(stream); + iv.write(out); } } 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 96% 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 c10f942..8790d3a 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java @@ -26,7 +26,7 @@ import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import static ch.dissem.bitmessage.utils.Security.sha512; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * A network message is exchanged between two nodes. @@ -48,7 +48,7 @@ public class NetworkMessage implements Streamable { * First 4 bytes of sha512(payload) */ private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException { - byte[] d = sha512(bytes); + byte[] d = security().sha512(bytes); return new byte[]{d[0], d[1], d[2], d[3]}; } 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 92% 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 0ef3340..99b3aec 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java @@ -24,12 +24,13 @@ import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.Security; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import static ch.dissem.bitmessage.utils.Singleton.security; + /** * The 'object' command sends an object that is shared throughout the network. */ @@ -89,7 +90,9 @@ public class ObjectMessage implements MessagePayload { } public InventoryVector getInventoryVector() { - return new InventoryVector(Bytes.truncate(Security.doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32)); + return new InventoryVector( + Bytes.truncate(security().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32) + ); } private boolean isEncrypted() { @@ -113,7 +116,7 @@ public class ObjectMessage implements MessagePayload { public void sign(PrivateKey key) { if (payload.isSigned()) { - payload.setSignature(Security.getSignature(getBytesToSign(), key)); + payload.setSignature(security().getSignature(getBytesToSign(), key)); } } @@ -147,12 +150,16 @@ public class ObjectMessage implements MessagePayload { public boolean isSignatureValid(Pubkey pubkey) throws IOException { if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first"); - return Security.isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); + return security().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); } @Override public void write(OutputStream out) throws IOException { - out.write(nonce); + if (nonce != null) { + out.write(nonce); + } else { + out.write(new byte[8]); + } out.write(getPayloadBytesWithoutNonce()); } 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 96% 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 28cebfa..fbd5d48 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.UnixTime; import java.io.*; import java.util.*; @@ -43,6 +44,7 @@ public class Plaintext implements Streamable { private Long received; private Set<Label> labels; + private byte[] initialHash; private Plaintext(Builder builder) { id = builder.id; @@ -63,6 +65,7 @@ public class Plaintext implements Streamable { public static Plaintext read(Type type, InputStream in) throws IOException { return readWithoutSignature(type, in) .signature(Decode.varBytes(in)) + .received(UnixTime.now()) .build(); } @@ -131,6 +134,15 @@ public class Plaintext implements Streamable { this.signature = signature; } + public boolean isUnread() { + for (Label label : labels) { + if (label.getType() == Label.Type.UNREAD) { + return true; + } + } + return false; + } + public void write(OutputStream out, boolean includeSignature) throws IOException { Encode.varInt(from.getVersion(), out); Encode.varInt(from.getStream(), out); @@ -249,6 +261,14 @@ public class Plaintext implements Streamable { } } + public void setInitialHash(byte[] initialHash) { + this.initialHash = initialHash; + } + + public byte[] getInitialHash() { + return initialHash; + } + public enum Encoding { IGNORE(0), TRIVIAL(1), SIMPLE(2); 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 91% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java index 9ee4cf9..cc12050 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java @@ -18,10 +18,11 @@ package ch.dissem.bitmessage.entity; import java.io.IOException; import java.io.OutputStream; +import java.io.Serializable; /** * An object that can be written to an {@link OutputStream} */ -public interface Streamable { +public interface Streamable extends Serializable { void write(OutputStream stream) throws IOException; } 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java rename to core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/entity/Version.java rename to core/src/main/java/ch/dissem/bitmessage/entity/Version.java 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 94% 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 2491d85..47bf539 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,11 +21,11 @@ import ch.dissem.bitmessage.entity.Encrypted; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.PlaintextHolder; import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.utils.Security; import java.io.IOException; import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * Users who are subscribed to the sending address will see the message appear in their inbox. @@ -78,7 +78,7 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai } public void encrypt() throws IOException { - encrypt(Security.createPublicKey(plaintext.getFrom().getPublicDecryptionKey()).getEncoded(false)); + encrypt(security().createPublicKey(plaintext.getFrom().getPublicDecryptionKey())); } @Override 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 68% 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 2018373..fe45ac5 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,28 +17,16 @@ package ch.dissem.bitmessage.entity.payload; import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.utils.*; -import org.bouncycastle.crypto.BufferedBlockCipher; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.InvalidCipherTextException; -import org.bouncycastle.crypto.engines.AESEngine; -import org.bouncycastle.crypto.modes.CBCBlockCipher; -import org.bouncycastle.crypto.paddings.PKCS7Padding; -import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; -import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.ParametersWithIV; -import org.bouncycastle.math.ec.ECFieldElement; -import org.bouncycastle.math.ec.ECPoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; -import java.math.BigInteger; import java.util.Arrays; import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; +import static ch.dissem.bitmessage.utils.Singleton.security; public class CryptoBox implements Streamable { @@ -46,35 +34,38 @@ public class CryptoBox implements Streamable { private final byte[] initializationVector; private final int curveType; - private final ECPoint R; + private final byte[] R; private final byte[] mac; private byte[] encrypted; - public CryptoBox(Streamable data, byte[] encryptionKey) throws IOException { - this(data, Security.keyToPoint(encryptionKey)); + private long addressVersion; + + + public CryptoBox(Streamable data, byte[] K) throws IOException { + this(Encode.bytes(data), K); } - public CryptoBox(Streamable data, ECPoint K) throws IOException { + public CryptoBox(byte[] data, byte[] K) throws IOException { curveType = 0x02CA; // 1. The destination public key is called K. // 2. Generate 16 random bytes using a secure random number generator. Call them IV. - initializationVector = Security.randomBytes(16); + initializationVector = security().randomBytes(16); // 3. Generate a new random EC key pair with private key called r and public key called R. - byte[] r = Security.randomBytes(PRIVATE_KEY_SIZE); - R = Security.createPublicKey(r); + byte[] r = security().randomBytes(PRIVATE_KEY_SIZE); + R = security().createPublicKey(r); // 4. Do an EC point multiply with public key K and private key r. This gives you public key P. - ECPoint P = K.multiply(Security.keyToBigInt(r)).normalize(); - byte[] X = P.getXCoord().getEncoded(); + byte[] P = security().multiply(K, r); + byte[] X = Points.getX(P); // 5. Use the X component of public key P and calculate the SHA512 hash H. - byte[] H = Security.sha512(X); + byte[] H = security().sha512(X); // 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. byte[] key_e = Arrays.copyOfRange(H, 0, 32); byte[] key_m = Arrays.copyOfRange(H, 32, 64); // 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7. // 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text. - encrypted = crypt(true, Encode.bytes(data), key_e); + encrypted = security().crypt(true, data, key_e, initializationVector); // 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. mac = calculateMac(key_m); @@ -84,7 +75,7 @@ public class CryptoBox implements Streamable { private CryptoBox(Builder builder) { initializationVector = builder.initializationVector; curveType = builder.curveType; - R = Security.createPoint(builder.xComponent, builder.yComponent); + R = security().createPoint(builder.xComponent, builder.yComponent); encrypted = builder.encrypted; mac = builder.mac; } @@ -102,18 +93,17 @@ public class CryptoBox implements Streamable { } /** - * @param privateKey a private key, typically should be 32 bytes long + * @param k a private key, typically should be 32 bytes long * @return an InputStream yielding the decrypted data * @throws DecryptionFailedException if the payload can't be decrypted using this private key * @see <a href='https://bitmessage.org/wiki/Encryption#Decryption'>https://bitmessage.org/wiki/Encryption#Decryption</a> */ - public InputStream decrypt(byte[] privateKey) throws DecryptionFailedException { + public InputStream decrypt(byte[] k) throws DecryptionFailedException { // 1. The private key used to decrypt is called k. - BigInteger k = Security.keyToBigInt(privateKey); // 2. Do an EC point multiply with private key k and public key R. This gives you public key P. - ECPoint P = R.multiply(k).normalize(); + byte[] P = security().multiply(R, k); // 3. Use the X component of public key P and calculate the SHA512 hash H. - byte[] H = Security.sha512(P.getXCoord().getEncoded()); + byte[] H = security().sha512(Arrays.copyOfRange(P, 1, 33)); // 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. byte[] key_e = Arrays.copyOfRange(H, 0, 32); byte[] key_m = Arrays.copyOfRange(H, 32, 64); @@ -126,49 +116,28 @@ public class CryptoBox implements Streamable { // 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key // and the cipher text as payload. The output is the padded input text. - return new ByteArrayInputStream(crypt(false, encrypted, key_e)); + return new ByteArrayInputStream(security().crypt(false, encrypted, key_e, initializationVector)); } private byte[] calculateMac(byte[] key_m) { try { ByteArrayOutputStream macData = new ByteArrayOutputStream(); writeWithoutMAC(macData); - return Security.mac(key_m, macData.toByteArray()); + return security().mac(key_m, macData.toByteArray()); } catch (IOException e) { throw new RuntimeException(e); } } - private byte[] crypt(boolean encrypt, byte[] data, byte[] key_e) { - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); - - CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); - - cipher.init(encrypt, params); - - byte[] buffer = new byte[cipher.getOutputSize(data.length)]; - int length = cipher.processBytes(data, 0, data.length, buffer, 0); - try { - length += cipher.doFinal(buffer, length); - } catch (InvalidCipherTextException e) { - throw new IllegalArgumentException(e); - } - if (length < buffer.length) { - return Arrays.copyOfRange(buffer, 0, length); - } - return buffer; - } - private void writeWithoutMAC(OutputStream out) throws IOException { out.write(initializationVector); Encode.int16(curveType, out); - writeCoordinateComponent(out, R.getXCoord()); - writeCoordinateComponent(out, R.getYCoord()); + writeCoordinateComponent(out, Points.getX(R)); + writeCoordinateComponent(out, Points.getY(R)); out.write(encrypted); } - private void writeCoordinateComponent(OutputStream out, ECFieldElement coord) throws IOException { - byte[] x = coord.getEncoded(); + private void writeCoordinateComponent(OutputStream out, byte[] x) throws IOException { int offset = Bytes.numberOfLeadingZeros(x); int length = x.length - offset; Encode.int16(length, out); @@ -195,7 +164,7 @@ public class CryptoBox implements Streamable { } public Builder curveType(int curveType) { - if (curveType != 0x2CA) LOG.debug("Unexpected curve type " + curveType); + if (curveType != 0x2CA) LOG.trace("Unexpected curve type " + curveType); this.curveType = curveType; return this; } 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 100% 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 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 100% 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 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 100% 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 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 98% 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 ef42718..0ca45cd 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,6 +21,7 @@ import ch.dissem.bitmessage.entity.Streamable; import java.io.IOException; import java.io.OutputStream; +import java.io.Serializable; /** * The payload of an 'object' command. This is shared by the network. 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 91% 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 2bdcbb6..1243b32 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 @@ -20,8 +20,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; -import static ch.dissem.bitmessage.utils.Security.ripemd160; -import static ch.dissem.bitmessage.utils.Security.sha512; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * Public keys for signing and encryption, the answer to a 'getpubkey' request. @@ -34,7 +33,7 @@ public abstract class Pubkey extends ObjectPayload { } public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) { - return ripemd160(sha512(publicSigningKey, publicEncryptionKey)); + return security().ripemd160(security().sha512(publicSigningKey, publicEncryptionKey)); } public abstract byte[] getSigningKey(); @@ -44,7 +43,7 @@ public abstract class Pubkey extends ObjectPayload { public abstract int getBehaviorBitfield(); public byte[] getRipe() { - return ripemd160(sha512(getSigningKey(), getEncryptionKey())); + return security().ripemd160(security().sha512(getSigningKey(), getEncryptionKey())); } public long getNonceTrialsPerByte() { 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 94% 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 366d26b..fc67422 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 @@ -21,9 +21,10 @@ import ch.dissem.bitmessage.utils.Strings; import java.io.IOException; import java.io.OutputStream; +import java.io.Serializable; import java.util.Arrays; -public class InventoryVector implements Streamable { +public class InventoryVector implements Streamable, Serializable { /** * Hash of the object */ @@ -37,7 +38,6 @@ public class InventoryVector implements Streamable { InventoryVector that = (InventoryVector) o; return Arrays.equals(hash, that.hash); - } @Override 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 e1bd8f2..7c37973 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 @@ -16,9 +16,10 @@ package ch.dissem.bitmessage.entity.valueobject; +import java.io.Serializable; import java.util.Objects; -public class Label { +public class Label implements Serializable { private Object id; private String label; private Type type; 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 100% 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 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 79% 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 f0956a6..d07c859 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 @@ -18,18 +18,18 @@ package ch.dissem.bitmessage.entity.valueobject; import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.payload.V3Pubkey; -import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.Security; import java.io.*; +import static ch.dissem.bitmessage.utils.Singleton.security; + /** - * Created by chris on 18.04.15. + * Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying + * {@link Pubkey} object. */ public class PrivateKey implements Streamable { public static final int PRIVATE_KEY_SIZE = 32; @@ -45,15 +45,15 @@ public class PrivateKey implements Streamable { byte[] pubEK; byte[] ripe; do { - privSK = Security.randomBytes(PRIVATE_KEY_SIZE); - privEK = Security.randomBytes(PRIVATE_KEY_SIZE); - pubSK = Security.createPublicKey(privSK).getEncoded(false); - pubEK = Security.createPublicKey(privEK).getEncoded(false); + privSK = security().randomBytes(PRIVATE_KEY_SIZE); + privEK = security().randomBytes(PRIVATE_KEY_SIZE); + pubSK = security().createPublicKey(privSK); + pubEK = security().createPublicKey(privEK); ripe = Pubkey.getRipe(pubSK, pubEK); } while (ripe[0] != 0 || (shorter && ripe[1] != 0)); this.privateSigningKey = privSK; this.privateEncryptionKey = privEK; - this.pubkey = Security.createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, + this.pubkey = security().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, nonceTrialsPerByte, extraBytes, features); } @@ -66,9 +66,9 @@ public class PrivateKey implements Streamable { public PrivateKey(long version, long stream, String passphrase, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { try { // FIXME: this is most definitely wrong - this.privateSigningKey = Bytes.truncate(Security.sha512(passphrase.getBytes("UTF-8"), new byte[]{0}), 32); - this.privateEncryptionKey = Bytes.truncate(Security.sha512(passphrase.getBytes("UTF-8"), new byte[]{1}), 32); - this.pubkey = Security.createPubkey(version, stream, privateSigningKey, privateEncryptionKey, + this.privateSigningKey = Bytes.truncate(security().sha512(passphrase.getBytes("UTF-8"), new byte[]{0}), 32); + this.privateEncryptionKey = Bytes.truncate(security().sha512(passphrase.getBytes("UTF-8"), new byte[]{1}), 32); + this.pubkey = security().createPubkey(version, stream, privateSigningKey, privateEncryptionKey, nonceTrialsPerByte, extraBytes, features); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/exception/NodeException.java rename to core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java 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 92% 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 a144bb6..33604ab 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -23,7 +23,6 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.NodeException; -import ch.dissem.bitmessage.utils.Security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +31,8 @@ import java.io.InputStream; import java.net.SocketException; import java.net.SocketTimeoutException; +import static ch.dissem.bitmessage.utils.Singleton.security; + /** * Creates {@link NetworkMessage} objects from {@link InputStream InputStreams} */ @@ -115,8 +116,8 @@ public class Factory { BitmessageAddress temp = new BitmessageAddress(address); PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey, createPubkey(temp.getVersion(), temp.getStream(), - Security.createPublicKey(privateSigningKey).getEncoded(false), - Security.createPublicKey(privateEncryptionKey).getEncoded(false), + security().createPublicKey(privateSigningKey), + security().createPublicKey(privateEncryptionKey), nonceTrialsPerByte, extraBytes, behaviourBitfield)); BitmessageAddress result = new BitmessageAddress(privateKey); if (!result.getAddress().equals(address)) { @@ -126,11 +127,17 @@ public class Factory { return result; } - public static BitmessageAddress generatePrivateAddress(boolean shorter, long stream, Pubkey.Feature... features) { + public static BitmessageAddress generatePrivateAddress(boolean shorter, + long stream, + Pubkey.Feature... features) { return new BitmessageAddress(new PrivateKey(shorter, stream, 1000, 1000, features)); } - static ObjectPayload getObjectPayload(long objectType, long version, long streamNumber, InputStream stream, int length) throws IOException { + static ObjectPayload getObjectPayload(long objectType, + long version, + long streamNumber, + InputStream stream, + int length) throws IOException { ObjectType type = ObjectType.fromNumber(objectType); if (type != null) { switch (type) { @@ -147,7 +154,7 @@ public class Factory { } } // fallback: just store the message - we don't really care what it is -// LOG.info("Unexpected object type: " + objectType); + LOG.trace("Unexpected object type: " + objectType); return GenericPayload.read(version, stream, streamNumber, length); } 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 92% 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 57d240d..d13e73e 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -24,7 +24,6 @@ import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.utils.AccessCounter; import ch.dissem.bitmessage.utils.Decode; -import ch.dissem.bitmessage.utils.Security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +32,7 @@ import java.io.IOException; import java.io.InputStream; import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * Creates protocol v3 network messages from {@link InputStream InputStreams} @@ -44,6 +44,9 @@ class V3MessageFactory { findMagic(in); String command = getCommand(in); int length = (int) Decode.uint32(in); + if (length > 1600003) { + throw new NodeException("Payload of " + length + " bytes received, no more than 1600003 was expected."); + } byte[] checksum = Decode.bytes(in, 4); byte[] payloadBytes = Decode.bytes(in, length); @@ -73,12 +76,18 @@ class V3MessageFactory { return parseGetData(stream); case "object": return readObject(stream, length); + case "custom": + return readCustom(stream, length); default: LOG.debug("Unknown command: " + command); return null; } } + private static MessagePayload readCustom(InputStream in, int length) throws IOException { + return CustomMessage.read(in, length); + } + public static ObjectMessage readObject(InputStream in, int length) throws IOException { AccessCounter counter = new AccessCounter(); byte nonce[] = Decode.bytes(in, 8, counter); @@ -92,7 +101,7 @@ class V3MessageFactory { try { ByteArrayInputStream dataStream = new ByteArrayInputStream(data); payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length); - } catch (IOException e) { + } catch (Exception e) { LOG.trace("Could not parse object payload - using generic payload instead", e); payload = new GenericPayload(version, stream, data); } @@ -174,7 +183,7 @@ class V3MessageFactory { } private static boolean testChecksum(byte[] checksum, byte[] payload) { - byte[] payloadChecksum = Security.sha512(payload); + byte[] payloadChecksum = security().sha512(payload); for (int i = 0; i < checksum.length; i++) { if (checksum[i] != payloadChecksum[i]) { return false; @@ -185,10 +194,10 @@ class V3MessageFactory { private static String getCommand(InputStream stream) throws IOException { byte[] bytes = new byte[12]; - int end = -1; + int end = bytes.length; for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) stream.read(); - if (end == -1) { + if (end == bytes.length) { if (bytes[i] == 0) end = i; } else { if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command"); diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java new file mode 100644 index 0000000..3cf3f0d --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java @@ -0,0 +1,183 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.UnixTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; + +import static ch.dissem.bitmessage.utils.Numbers.max; + +/** + * Implements everything that isn't directly dependent on either Spongy- or Bouncycastle. + */ +public abstract class AbstractCryptography implements Cryptography, InternalContext.ContextHolder { + public static final Logger LOG = LoggerFactory.getLogger(Cryptography.class); + private static final SecureRandom RANDOM = new SecureRandom(); + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger TWO_POW_64 = TWO.pow(64); + private static final BigInteger TWO_POW_16 = TWO.pow(16); + + private final String provider; + private InternalContext context; + + protected AbstractCryptography(String provider) { + this.provider = provider; + } + + @Override + public void setContext(InternalContext context) { + this.context = context; + } + + public byte[] sha512(byte[]... data) { + return hash("SHA-512", data); + } + + public byte[] doubleSha512(byte[]... data) { + MessageDigest mda = md("SHA-512"); + for (byte[] d : data) { + mda.update(d); + } + return mda.digest(mda.digest()); + } + + public byte[] doubleSha512(byte[] data, int length) { + MessageDigest mda = md("SHA-512"); + mda.update(data, 0, length); + return mda.digest(mda.digest()); + } + + public byte[] ripemd160(byte[]... data) { + return hash("RIPEMD160", data); + } + + public byte[] doubleSha256(byte[] data, int length) { + MessageDigest mda = md("SHA-256"); + mda.update(data, 0, length); + return mda.digest(mda.digest()); + } + + public byte[] sha1(byte[]... data) { + return hash("SHA-1", data); + } + + public byte[] randomBytes(int length) { + byte[] result = new byte[length]; + RANDOM.nextBytes(result); + return result; + } + + public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, + long extraBytes, ProofOfWorkEngine.Callback callback) { + nonceTrialsPerByte = max(nonceTrialsPerByte, context.getNetworkNonceTrialsPerByte()); + extraBytes = max(extraBytes, context.getNetworkExtraBytes()); + + byte[] initialHash = getInitialHash(object); + + byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); + + context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback); + } + + public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) + throws IOException { + byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); + byte[] value = doubleSha512(object.getNonce(), getInitialHash(object)); + if (Bytes.lt(target, value, 8)) { + throw new InsufficientProofOfWorkException(target, value); + } + } + + @Override + public byte[] getInitialHash(ObjectMessage object) { + return sha512(object.getPayloadBytesWithoutNonce()); + } + + @Override + public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + if (nonceTrialsPerByte == 0) nonceTrialsPerByte = context.getNetworkNonceTrialsPerByte(); + if (extraBytes == 0) extraBytes = context.getNetworkExtraBytes(); + + BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now()); + BigInteger numerator = TWO_POW_64; + BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes); + BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte) + .multiply( + powLength.add( + powLength.multiply(TTL).divide(TWO_POW_16) + ) + ); + return Bytes.expand(numerator.divide(denominator).toByteArray(), 8); + } + + private byte[] hash(String algorithm, byte[]... data) { + MessageDigest mda = md(algorithm); + for (byte[] d : data) { + mda.update(d); + } + return mda.digest(); + } + + private MessageDigest md(String algorithm) { + try { + return MessageDigest.getInstance(algorithm, provider); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public byte[] mac(byte[] key_m, byte[] data) { + try { + Mac mac = Mac.getInstance("HmacSHA256", provider); + mac.init(new SecretKeySpec(key_m, "HmacSHA256")); + return mac.doFinal(data); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey, + long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { + return Factory.createPubkey(version, stream, + createPublicKey(privateSigningKey), + createPublicKey(privateEncryptionKey), + nonceTrialsPerByte, extraBytes, features); + } + + public BigInteger keyToBigInt(byte[] privateKey) { + return new BigInteger(1, privateKey); + } + + public long randomNonce() { + return RANDOM.nextLong(); + } +} 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java rename to core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java b/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java new file mode 100644 index 0000000..48739ea --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java @@ -0,0 +1,210 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.SecureRandom; + +/** + * Provides some methods to help with hashing and encryption. All randoms are created using {@link SecureRandom}, + * which should be secure enough. + */ +public interface Cryptography { + /** + * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at + * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in + * success on the same thread. + * + * @param data to get hashed + * @return SHA-512 hash of data + */ + byte[] sha512(byte[]... data); + + /** + * A helper method to calculate doubleSHA-512 hashes. Please note that a new {@link MessageDigest} object is created + * at each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in + * success on the same thread. + * + * @param data to get hashed + * @return SHA-512 hash of data + */ + byte[] doubleSha512(byte[]... data); + + /** + * A helper method to calculate double SHA-512 hashes. This method allows to only use a part of the available bytes + * to use for the hash calculation. + * <p> + * Please note that a new {@link MessageDigest} object is created at each call (to ensure thread safety), so you + * shouldn't use this if you need to do many hash calculations in short order on the same thread. + * </p> + * + * @param data to get hashed + * @param length number of bytes to be taken into account + * @return SHA-512 hash of data + */ + byte[] doubleSha512(byte[] data, int length); + + /** + * A helper method to calculate RIPEMD-160 hashes. Supplying multiple byte arrays has the same result as a + * concatenation of all arrays, but might perform better. + * <p> + * Please note that a new {@link MessageDigest} object is created at + * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short + * order on the same thread. + * </p> + * + * @param data to get hashed + * @return RIPEMD-160 hash of data + */ + byte[] ripemd160(byte[]... data); + + /** + * A helper method to calculate double SHA-256 hashes. This method allows to only use a part of the available bytes + * to use for the hash calculation. + * <p> + * Please note that a new {@link MessageDigest} object is created at + * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short + * order on the same thread. + * </p> + * + * @param data to get hashed + * @param length number of bytes to be taken into account + * @return SHA-256 hash of data + */ + byte[] doubleSha256(byte[] data, int length); + + /** + * A helper method to calculate SHA-1 hashes. Supplying multiple byte arrays has the same result as a + * concatenation of all arrays, but might perform better. + * <p> + * Please note that a new {@link MessageDigest} object is created at + * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short + * order on the same thread. + * </p> + * + * @param data to get hashed + * @return SHA hash of data + */ + byte[] sha1(byte[]... data); + + /** + * @param length number of bytes to return + * @return an array of the given size containing random bytes + */ + byte[] randomBytes(int length); + + /** + * Calculates the proof of work. This might take a long time, depending on the hardware, message size and time to + * live. + * + * @param object to do the proof of work for + * @param nonceTrialsPerByte difficulty + * @param extraBytes bytes to add to the object size (makes it more difficult to send small messages) + * @param callback to handle nonce once it's calculated + */ + void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, + long extraBytes, ProofOfWorkEngine.Callback callback); + + /** + * @param object to be checked + * @param nonceTrialsPerByte difficulty + * @param extraBytes bytes to add to the object size + * @throws InsufficientProofOfWorkException if proof of work doesn't check out (makes it more difficult to send small messages) + */ + void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) + throws IOException; + + byte[] getInitialHash(ObjectMessage object); + + byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes); + + /** + * Calculates the MAC for a message (data) + * + * @param key_m the symmetric key used + * @param data the message data to calculate the MAC for + * @return the MAC + */ + byte[] mac(byte[] key_m, byte[] data); + + /** + * @param encrypt if true, encrypts data, otherwise tries to decrypt it. + * @param data + * @param key_e + * @return + */ + byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector); + + /** + * Create a new public key fom given private keys. + * + * @param version of the public key / address + * @param stream of the address + * @param privateSigningKey private key used for signing + * @param privateEncryptionKey private key used for encryption + * @param nonceTrialsPerByte proof of work difficulty + * @param extraBytes bytes to add for the proof of work (make it harder for small messages) + * @param features of the address + * @return a public key object + */ + Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey, + long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features); + + /** + * @param privateKey private key as byte array + * @return a public key corresponding to the given private key + */ + byte[] createPublicKey(byte[] privateKey); + + /** + * @param privateKey private key as byte array + * @return a big integer representation (unsigned) of the given bytes + */ + BigInteger keyToBigInt(byte[] privateKey); + + /** + * @param data to check + * @param signature the signature of the message + * @param pubkey the sender's public key + * @return true if the signature is valid, false otherwise + */ + boolean isSignatureValid(byte[] data, byte[] signature, Pubkey pubkey); + + /** + * Calculate the signature of data, using the given private key. + * + * @param data to be signed + * @param privateKey to be used for signing + * @return the signature + */ + byte[] getSignature(byte[] data, ch.dissem.bitmessage.entity.valueobject.PrivateKey privateKey); + + /** + * @return a random number of type long + */ + long randomNonce(); + + byte[] multiply(byte[] k, byte[] r); + + byte[] createPoint(byte[] x, byte[] y); +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java b/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java similarity index 57% rename from domain/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java rename to core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java index ed2f38a..8e49586 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java @@ -16,25 +16,12 @@ package ch.dissem.bitmessage.ports; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.utils.Property; - -import java.io.IOException; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.MessagePayload; /** - * Handles incoming messages + * @author Christian Basler */ -public interface NetworkHandler { - void start(MessageListener listener); - - void stop(); - - void offer(InventoryVector iv); - - Property getNetworkStatus(); - - interface MessageListener { - void receive(ObjectMessage object) throws IOException; - } +public interface CustomCommandHandler { + MessagePayload handle(CustomMessage request); } 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 67% rename from domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java rename to core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java index 934a4d0..6af65c1 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java @@ -26,15 +26,32 @@ import java.util.List; * The Inventory stores and retrieves objects, cleans up outdated objects and can tell which objects are still missing. */ public interface Inventory { + /** + * Returns the IVs of all valid objects we have for the given streams + */ List<InventoryVector> getInventory(long... streams); + /** + * Returns the IVs of all objects in the offer that we don't have already. Implementations are allowed to + * ignore the streams parameter, but it must be set when calling this method. + */ List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams); ObjectMessage getObject(InventoryVector vector); + /** + * This method is mainly used to search for public keys to newly added addresses or broadcasts from new + * subscriptions. + */ List<ObjectMessage> getObjects(long stream, long version, ObjectType... types); void storeObject(ObjectMessage object); + boolean contains(ObjectMessage object); + + /** + * Deletes all objects that expired 5 minutes ago or earlier + * (so we don't accidentally request objects we just deleted) + */ void cleanup(); } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/MemoryNodeRegistry.java b/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java similarity index 78% rename from repositories/src/main/java/ch/dissem/bitmessage/repository/MemoryNodeRegistry.java rename to core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java index 15a4134..8d43423 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/MemoryNodeRegistry.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package ch.dissem.bitmessage.repository; +package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.ports.NodeRegistry; import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,10 +34,10 @@ import static java.util.Collections.newSetFromMap; public class MemoryNodeRegistry implements NodeRegistry { private static final Logger LOG = LoggerFactory.getLogger(MemoryNodeRegistry.class); - private final Map<Long, Set<NetworkAddress>> stableNodes = new HashMap<>(); + private final Map<Long, Set<NetworkAddress>> stableNodes = new ConcurrentHashMap<>(); private final Map<Long, Set<NetworkAddress>> knownNodes = new ConcurrentHashMap<>(); - public MemoryNodeRegistry() { + private void loadStableNodes() { try (InputStream in = getClass().getClassLoader().getResourceAsStream("nodes.txt")) { Scanner scanner = new Scanner(in); long stream = 0; @@ -56,14 +55,21 @@ public class MemoryNodeRegistry implements NodeRegistry { stableNodes.put(stream, streamSet); } else if (streamSet != null) { int portIndex = line.lastIndexOf(':'); - InetAddress inetAddress = InetAddress.getByName(line.substring(0, portIndex)); + InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex)); int port = Integer.valueOf(line.substring(portIndex + 1)); - streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build()); + for (InetAddress inetAddress : inetAddresses) { + streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build()); + } } } catch (IOException e) { LOG.warn(e.getMessage(), e); } } + if (LOG.isDebugEnabled()) { + for (Map.Entry<Long, Set<NetworkAddress>> e : stableNodes.entrySet()) { + LOG.debug("Stream " + e.getKey() + ": loaded " + e.getValue().size() + " bootstrap nodes."); + } + } } catch (IOException e) { throw new RuntimeException(e); } @@ -82,9 +88,16 @@ public class MemoryNodeRegistry implements NodeRegistry { known.remove(node); } } - } else if (stableNodes.containsKey(stream)) { - // To reduce load on stable nodes, only return one - result.add(selectRandom(stableNodes.get(stream))); + } else { + Set<NetworkAddress> nodes = stableNodes.get(stream); + if (nodes == null || nodes.isEmpty()) { + loadStableNodes(); + nodes = stableNodes.get(stream); + } + if (nodes != null && !nodes.isEmpty()) { + // To reduce load on stable nodes, only return one + result.add(selectRandom(nodes)); + } } } return selectRandom(limit, result); 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 89% 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 b698a97..9e949a7 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java @@ -28,12 +28,18 @@ public interface MessageRepository { List<Label> getLabels(Label.Type... types); + int countUnread(Label label); + + Plaintext getMessage(byte[] initialHash); + List<Plaintext> findMessages(Label label); List<Plaintext> findMessages(Status status); List<Plaintext> findMessages(Status status, BitmessageAddress recipient); + List<Plaintext> findMessages(BitmessageAddress sender); + void save(Plaintext message); void remove(Plaintext message); 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 50% 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 9814c3a..790e3b7 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java @@ -24,6 +24,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Semaphore; import static ch.dissem.bitmessage.utils.Bytes.inc; @@ -31,46 +32,53 @@ import static ch.dissem.bitmessage.utils.Bytes.inc; * A POW engine using all available CPU cores. */ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { - private static Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class); + private static final Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class); + private static final Semaphore semaphore = new Semaphore(1, true); + /** + * This method will block until all pending nonce calculations are done, but not wait for its own calculation + * to finish. + * (This implementation becomes very inefficient if multiple nonce are calculated at the same time.) + * + * @param initialHash the SHA-512 hash of the object to send, sans nonce + * @param target the target, representing an unsigned long + * @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make + */ @Override - public byte[] calculateNonce(byte[] initialHash, byte[] target) { + public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + callback = new CallbackWrapper(callback); int cores = Runtime.getRuntime().availableProcessors(); if (cores > 255) cores = 255; LOG.info("Doing POW using " + cores + " cores"); - long time = System.currentTimeMillis(); List<Worker> workers = new ArrayList<>(cores); for (int i = 0; i < cores; i++) { - Worker w = new Worker(workers, (byte) cores, i, initialHash, target); + Worker w = new Worker(workers, (byte) cores, i, initialHash, target, callback); workers.add(w); } for (Worker w : workers) { + // Doing this in the previous loop might cause a ConcurrentModificationException in the worker + // if a worker finds a nonce while new ones are still being added. w.start(); } - for (Worker w : workers) { - try { - w.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - if (w.isSuccessful()) { - LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - time) / 1000) + " seconds"); - return w.getNonce(); - } - } - throw new RuntimeException("All workers ended without yielding a nonce - something is seriously broken!"); } private static class Worker extends Thread { + private final Callback callback; private final byte numberOfCores; private final List<Worker> workers; private final byte[] initialHash; private final byte[] target; private final MessageDigest mda; private final byte[] nonce = new byte[8]; - private boolean successful = false; - public Worker(List<Worker> workers, byte numberOfCores, int core, byte[] initialHash, byte[] target) { + public Worker(List<Worker> workers, byte numberOfCores, int core, byte[] initialHash, byte[] target, + Callback callback) { + this.callback = callback; this.numberOfCores = numberOfCores; this.workers = workers; this.initialHash = initialHash; @@ -84,14 +92,6 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { } } - public boolean isSuccessful() { - return successful; - } - - public byte[] getNonce() { - return nonce; - } - @Override public void run() { do { @@ -99,13 +99,43 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { mda.update(nonce); mda.update(initialHash); if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) { - successful = true; - for (Worker w : workers) { - w.interrupt(); + synchronized (callback) { + if (!Thread.interrupted()) { + for (Worker w : workers) { + w.interrupt(); + } + // Clear interrupted flag for callback + Thread.interrupted(); + callback.onNonceCalculated(initialHash, nonce); + } } return; } } while (!Thread.interrupted()); } } + + public static class CallbackWrapper implements Callback { + private final Callback callback; + private final long startTime; + private boolean waiting = true; + + public CallbackWrapper(Callback callback) { + this.startTime = System.currentTimeMillis(); + this.callback = callback; + } + + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + // Prevents the callback from being called twice if two nonces are found simultaneously + synchronized (this) { + if (waiting) { + semaphore.release(); + LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds"); + waiting = false; + callback.onNonceCalculated(initialHash, nonce); + } + } + } + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java b/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java new file mode 100644 index 0000000..909d3dd --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.utils.Property; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.concurrent.Future; + +/** + * Handles incoming messages + */ +public interface NetworkHandler { + /** + * Connects to the trusted host, fetches and offers new messages and disconnects afterwards. + * <p> + * An implementation should disconnect if either the timeout is reached or the returned thread is interrupted. + * </p> + */ + Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds); + + /** + * Send a custom message to a specific node (that should implement handling for this message type) and returns + * the response, which in turn is expected to be a {@link CustomMessage}. + * + * @param server the node's address + * @param port the node's port + * @param request the request + * @return the response + */ + CustomMessage send(InetAddress server, int port, CustomMessage request); + + /** + * Start a full network node, accepting incoming connections and relaying objects. + */ + void start(MessageListener listener); + + /** + * Stop the full network node. + */ + void stop(); + + /** + * Offer new objects to up to 8 random nodes. + */ + void offer(InventoryVector iv); + + Property getNetworkStatus(); + + boolean isRunning(); + + interface MessageListener { + void receive(ObjectMessage object) throws IOException; + } +} 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 61% 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 31f6657..fc7b4c2 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java @@ -24,9 +24,17 @@ public interface ProofOfWorkEngine { * Returns a nonce, such that the first 8 bytes from sha512(sha512(nonce||initialHash)) represent a unsigned long * smaller than target. * - * @param initialHash the SHA-512 hash of the object to send, sans nonce - * @param target the target, representing an unsigned long - * @return 8 bytes nonce + * @param initialHash the SHA-512 hash of the object to send, sans nonce + * @param target the target, representing an unsigned long + * @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make + * sure this is only called once. */ - byte[] calculateNonce(byte[] initialHash, byte[] target); + void calculateNonce(byte[] initialHash, byte[] target, Callback callback); + + interface Callback { + /** + * @param nonce 8 bytes nonce + */ + void onNonceCalculated(byte[] initialHash, byte[] nonce); + } } 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 76% 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 0d9d392..e8d649b 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java @@ -23,11 +23,15 @@ import java.security.MessageDigest; import static ch.dissem.bitmessage.utils.Bytes.inc; /** - * Created by chris on 14.04.15. + * You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one. + * <p> + * <strong>Warning:</strong> implementations probably depend on POW being asynchronous, that's + * another reason not to use this one. + * </p> */ public class SimplePOWEngine implements ProofOfWorkEngine { @Override - public byte[] calculateNonce(byte[] initialHash, byte[] target) { + public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { byte[] nonce = new byte[8]; MessageDigest mda; try { @@ -40,6 +44,6 @@ public class SimplePOWEngine implements ProofOfWorkEngine { mda.update(nonce); mda.update(initialHash); } while (Bytes.lt(target, mda.digest(mda.digest()), 8)); - return nonce; + callback.onNonceCalculated(initialHash, nonce); } } 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Base58.java 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 97% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java index 31a3dcc..8107eb0 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -16,12 +16,6 @@ package ch.dissem.bitmessage.utils; -import ch.dissem.bitmessage.entity.Streamable; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Arrays; - /** * A helper class for working with byte arrays interpreted as unsigned big endian integers. * This is one part due to the fact that Java doesn't support unsigned numbers, and another @@ -91,6 +85,8 @@ public class Bytes { if (a < 0) return b < 0 && a < b; if (b < 0) return a >= 0 || a < b; return a < b; + // This would be easier to understand, but is (slightly) slower: + // return (a & 0xff) < (b & 0xff); } /** diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java b/core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java new file mode 100644 index 0000000..775a5f3 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +/** + * Waits for a value within a callback method to be set. + */ +public class CallbackWaiter<T> { + private final long startTime = System.currentTimeMillis(); + private volatile boolean isSet; + private T value; + private long time; + + public void setValue(T value) { + synchronized (this) { + this.time = System.currentTimeMillis() - startTime; + this.value = value; + this.isSet = true; + } + } + + public T waitForValue() throws InterruptedException { + while (!isSet) { + Thread.sleep(100); + } + synchronized (this) { + return value; + } + } + + public long getTime() { + return time; + } +} 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 89% 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 095fb78..2cdc262 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java @@ -103,20 +103,30 @@ public class Encode { inc(counter, 8); } - public static void varString(String value, OutputStream stream) throws IOException { + public static void varString(String value, OutputStream out) throws IOException { byte[] bytes = value.getBytes("utf-8"); - // FIXME: technically, it says the length in characters, but I think this one might be correct + // Technically, it says the length in characters, but I think this one might be correct. + // It doesn't really matter, as only ASCII characters are being used. // see also Decode#varString() - varInt(bytes.length, stream); - stream.write(bytes); + varInt(bytes.length, out); + out.write(bytes); + } + + public static void varBytes(byte[] data, OutputStream out) throws IOException { + varInt(data.length, out); + out.write(data); } /** + * Serializes a {@link Streamable} object and returns the byte array. + * * @param streamable the object to be serialized * @return an array of bytes representing the given streamable object. * @throws IOException if an I/O error occurs. */ public static byte[] bytes(Streamable streamable) throws IOException { + if (streamable == null) return null; + ByteArrayOutputStream stream = new ByteArrayOutputStream(); streamable.write(stream); return stream.toByteArray(); 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..b1ace02 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java @@ -0,0 +1,10 @@ +package ch.dissem.bitmessage.utils; + +/** + * Created by chrig on 07.12.2015. + */ +public class Numbers { + public static long max(long a, long b) { + return a > b ? a : b; + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Points.java b/core/src/main/java/ch/dissem/bitmessage/utils/Points.java new file mode 100644 index 0000000..937fe20 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Points.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import java.util.Arrays; + +/** + * Created by chris on 20.07.15. + */ +public class Points { + public static byte[] getX(byte[] P) { + return Arrays.copyOfRange(P, 1, ((P.length - 1) / 2) + 1); + } + + public static byte[] getY(byte[] P) { + return Arrays.copyOfRange(P, ((P.length - 1) / 2) + 1, P.length); + } +} 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 56% 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 1030513..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,8 +16,16 @@ package ch.dissem.bitmessage.utils; +import java.util.Arrays; +import java.util.Objects; + /** - * Created by chris on 14.06.15. + * Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now + * used to contain different status information. It is by default displayed in some JSON inspired human readable + * notation, but you might only want to rely on the 'human readable' part. + * <p> + * If you need a real JSON representation, please add a method <code>toJson()</code>. + * </p> */ public class Property { private String name; @@ -30,6 +38,36 @@ public class Property { this.properties = properties; } + public String getName() { + return name; + } + + public Object getValue() { + return value; + } + + /** + * Returns the property if available or <code>null</code> otherwise. + * Subproperties can be requested by submitting the sequence of properties. + */ + public Property getProperty(String... name) { + if (name == null || name.length == 0) return null; + + for (Property p : properties) { + if (Objects.equals(name[0], p.name)) { + if (name.length == 1) + return p; + else + return p.getProperty(Arrays.copyOfRange(name, 1, name.length)); + } + } + return null; + } + + public Property[] getProperties() { + return properties; + } + @Override public String toString() { return toString(""); diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java b/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java new file mode 100644 index 0000000..0c7134b --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import ch.dissem.bitmessage.ports.Cryptography; + +/** + * Created by chris on 20.07.15. + */ +public class Singleton { + private static Cryptography cryptography; + + public static void initialize(Cryptography cryptography) { + synchronized (Singleton.class) { + if (Singleton.cryptography == null) { + Singleton.cryptography = cryptography; + } + } + } + + public static Cryptography security() { + return cryptography; + } +} 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 100% rename from domain/src/main/java/ch/dissem/bitmessage/utils/Strings.java rename to core/src/main/java/ch/dissem/bitmessage/utils/Strings.java 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/core/src/main/resources/nodes.txt b/core/src/main/resources/nodes.txt new file mode 100644 index 0000000..9466a85 --- /dev/null +++ b/core/src/main/resources/nodes.txt @@ -0,0 +1,8 @@ +[stream 1] + +dissem.ch:8444 +bootstrap8080.bitmessage.org:8080 +bootstrap8444.bitmessage.org:8444 + +[stream 2] +# none yet \ No newline at end of file diff --git a/domain/src/test/java/ch/dissem/bitmessage/DecryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java similarity index 96% rename from domain/src/test/java/ch/dissem/bitmessage/DecryptionTest.java rename to core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java index ebb3707..21f9506 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/DecryptionTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.V4Broadcast; import ch.dissem.bitmessage.entity.payload.V5Broadcast; import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; @@ -29,7 +30,7 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -public class DecryptionTest { +public class DecryptionTest extends TestBase { @Test public void ensureV4BroadcastIsDecryptedCorrectly() throws IOException, DecryptionFailedException { BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); diff --git a/domain/src/test/java/ch/dissem/bitmessage/EncryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java similarity index 91% rename from domain/src/test/java/ch/dissem/bitmessage/EncryptionTest.java rename to core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java index f0f8ddb..9a24842 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/EncryptionTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java @@ -24,20 +24,20 @@ import ch.dissem.bitmessage.entity.payload.GenericPayload; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.utils.Security; +import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; import java.io.IOException; +import static ch.dissem.bitmessage.utils.Singleton.security; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -public class EncryptionTest { +public class EncryptionTest extends TestBase { @Test public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException { - GenericPayload before = new GenericPayload(0, 1, Security.randomBytes(100)); + GenericPayload before = new GenericPayload(0, 1, security().randomBytes(100)); PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey()); diff --git a/domain/src/test/java/ch/dissem/bitmessage/SignatureTest.java b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java similarity index 96% rename from domain/src/test/java/ch/dissem/bitmessage/SignatureTest.java rename to core/src/test/java/ch/dissem/bitmessage/SignatureTest.java index dc2eb85..71b7d2a 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/SignatureTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java @@ -25,6 +25,7 @@ import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; @@ -33,7 +34,7 @@ import java.util.Date; import static org.junit.Assert.*; -public class SignatureTest { +public class SignatureTest extends TestBase { @Test public void ensureValidationWorks() throws IOException { ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); 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 95% 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 85b8098..e1fcc7f 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.Arrays; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; +import static ch.dissem.bitmessage.utils.Singleton.security; import static org.junit.Assert.*; public class BitmessageAddressTest { @@ -102,7 +103,7 @@ public class BitmessageAddressTest { System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n"); BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - Security.createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))); + security().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))); assertEquals(address_string, address.getAddress()); } @@ -119,7 +120,7 @@ public class BitmessageAddressTest { if (bytes.length != 37) throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); - byte[] hash = Security.doubleSha256(bytes, 33); + byte[] hash = security().doubleSha256(bytes, 33); for (int i = 0; i < 4; i++) { if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat); } @@ -132,7 +133,7 @@ public class BitmessageAddressTest { byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - Security.createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); + security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); } 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 61% 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 7e56f95..1bcb8e7 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -17,21 +17,23 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import static ch.dissem.bitmessage.utils.Singleton.security; +import static org.junit.Assert.*; -public class SerializationTest { +public class SerializationTest extends TestBase { @Test public void ensureGetPubkeyIsDeserializedAndSerializedCorrectly() throws IOException { doTest("V2GetPubkey.payload", 2, GetPubkey.class); @@ -75,7 +77,7 @@ public class SerializationTest { } @Test - public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws IOException, DecryptionFailedException { + public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws Exception { Plaintext p1 = new Plaintext.Builder(MSG) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) @@ -87,16 +89,57 @@ public class SerializationTest { p1.write(out); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); Plaintext p2 = Plaintext.read(MSG, in); + + // Received is automatically set on deserialization, so we'll need to set it to 0 + Field received = Plaintext.class.getDeclaredField("received"); + received.setAccessible(true); + received.set(p2, 0L); + assertEquals(p1, p2); } + @Test + public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { + ArrayList<InventoryVector> ivs = new ArrayList<>(50000); + for (int i = 0; i < 50000; i++) { + ivs.add(new InventoryVector(security().randomBytes(32))); + } + + Inv inv = new Inv.Builder().inventory(ivs).build(); + NetworkMessage before = new NetworkMessage(inv); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + before.write(out); + + NetworkMessage after = Factory.getNetworkMessage(3, new ByteArrayInputStream(out.toByteArray())); + Inv invAfter = (Inv) after.getPayload(); + assertEquals(ivs, invAfter.getInventory()); + } + private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException { byte[] data = TestUtils.getBytes(resourceName); InputStream in = new ByteArrayInputStream(data); ObjectMessage object = Factory.getObjectMessage(version, in, data.length); ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertNotNull(object); object.write(out); assertArrayEquals(data, out.toByteArray()); assertEquals(expectedPayloadType.getCanonicalName(), object.getPayload().getClass().getCanonicalName()); } + + @Test + public void ensureSystemSerializationWorks() throws Exception { + Plaintext plaintext = new Plaintext.Builder(MSG) + .from(TestUtils.loadContact()) + .to(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .labels(Collections.singletonList(new Label("Test", Label.Type.INBOX, 0))) + .message("Test", "Test Test.\nTest") + .build(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out); + oos.writeObject(plaintext); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(in); + assertEquals(plaintext, ois.readObject()); + } } diff --git a/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java b/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java new file mode 100644 index 0000000..1ed4aac --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.CallbackWaiter; +import ch.dissem.bitmessage.utils.TestBase; +import org.junit.Test; + +import static ch.dissem.bitmessage.utils.Singleton.security; +import static org.junit.Assert.assertTrue; + +public class ProofOfWorkEngineTest extends TestBase { + @Test(timeout = 90_000) + public void testSimplePOWEngine() throws InterruptedException { + testPOW(new SimplePOWEngine()); + } + + @Test(timeout = 90_000) + public void testThreadedPOWEngine() throws InterruptedException { + testPOW(new MultiThreadedPOWEngine()); + } + + private void testPOW(ProofOfWorkEngine engine) throws InterruptedException { + byte[] initialHash = security().sha512(new byte[]{1, 3, 6, 4}); + byte[] target = {0, 0, 0, -1, -1, -1, -1, -1}; + + final CallbackWaiter<byte[]> waiter1 = new CallbackWaiter<>(); + engine.calculateNonce(initialHash, target, + new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + waiter1.setValue(nonce); + } + }); + byte[] nonce = waiter1.waitForValue(); + System.out.println("Calculating nonce took " + waiter1.getTime() + "ms"); + assertTrue(Bytes.lt(security().doubleSha512(nonce, initialHash), target, 8)); + + // Let's add a second (shorter) run to find possible multi threading issues + byte[] initialHash2 = security().sha512(new byte[]{1, 3, 6, 5}); + byte[] target2 = {0, 0, -1, -1, -1, -1, -1, -1}; + + final CallbackWaiter<byte[]> waiter2 = new CallbackWaiter<>(); + engine.calculateNonce(initialHash2, target2, + new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + waiter2.setValue(nonce); + } + }); + byte[] nonce2 = waiter2.waitForValue(); + System.out.println("Calculating nonce took " + waiter2.getTime() + "ms"); + assertTrue(Bytes.lt(security().doubleSha512(nonce2, initialHash2), target2, 8)); + assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime()); + } + +} 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 81% rename from domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java index 7c83724..1af8d37 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java @@ -16,6 +16,7 @@ package ch.dissem.bitmessage.utils; +import org.junit.Ignore; import org.junit.Test; import java.io.IOException; @@ -25,16 +26,13 @@ import java.util.Random; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -/** - * Created by chris on 10.04.15. - */ public class BytesTest { public static final Random rnd = new Random(); @Test public void ensureExpandsCorrectly() { byte[] source = {1}; - byte[] expected = {0,1}; + byte[] expected = {0, 1}; assertArrayEquals(expected, Bytes.expand(source, 2)); } @@ -56,6 +54,24 @@ public class BytesTest { } } + /** + * This test is used to compare different implementations of the single byte lt comparison. It an safely be ignored. + */ + @Test + @Ignore + public void testLowerThanSingleByte() { + byte[] a = new byte[1]; + byte[] b = new byte[1]; + for (int i = 0; i < 255; i++) { + for (int j = 0; j < 255; j++) { + System.out.println("a = " + i + "\tb = " + j); + a[0] = (byte) i; + b[0] = (byte) j; + assertEquals(i < j, Bytes.lt(a, b)); + } + } + } + @Test public void testLowerThan() { for (int i = 0; i < 1000; i++) { 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 96% rename from domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java index 8c18ee7..60d882f 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java @@ -22,9 +22,6 @@ import java.io.*; import static org.junit.Assert.assertEquals; -/** - * Created by chris on 20.03.15. - */ public class DecodeTest { @Test public void ensureDecodingWorks() throws Exception { 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 3489112..aaba5bf 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java @@ -23,9 +23,6 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; -/** - * Created by chris on 13.03.15. - */ public class EncodeTest { @Test public void testUint8() throws IOException { 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/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java new file mode 100644 index 0000000..e757d91 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; + +/** + * Created by chris on 20.07.15. + */ +public class TestBase { + static { + Singleton.initialize(new BouncyCryptography()); + } +} 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/cryptography-bc/build.gradle b/cryptography-bc/build.gradle new file mode 100644 index 0000000..c09b0db --- /dev/null +++ b/cryptography-bc/build.gradle @@ -0,0 +1,18 @@ +uploadArchives { + repositories { + mavenDeployer { + pom.project { + name 'Jabit Bouncy Cryptography' + artifactId = 'jabit-cryptography-bouncy' + description 'The Cryptography implementation using bouncy castle' + } + } + } +} + +dependencies { + 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/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java b/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java new file mode 100644 index 0000000..28be67a --- /dev/null +++ b/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java @@ -0,0 +1,153 @@ +/* + * 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.cryptography.bc; + +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.ports.AbstractCryptography; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPrivateKeySpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.KeySpec; +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 BouncyCryptography extends AbstractCryptography { + private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); + + static { + java.security.Security.addProvider(new BouncyCastleProvider()); + } + + public BouncyCryptography() { + super("BC"); + } + + @Override + public byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector) { + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); + + CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); + + cipher.init(encrypt, params); + + byte[] buffer = new byte[cipher.getOutputSize(data.length)]; + int length = cipher.processBytes(data, 0, data.length, buffer, 0); + try { + length += cipher.doFinal(buffer, length); + } catch (InvalidCipherTextException e) { + throw new IllegalArgumentException(e); + } + if (length < buffer.length) { + return Arrays.copyOfRange(buffer, 0, length); + } + return buffer; + } + + @Override + public byte[] createPublicKey(byte[] privateKey) { + return EC_CURVE_PARAMETERS.getG().multiply(keyToBigInt(privateKey)).normalize().getEncoded(false); + } + + private ECPoint keyToPoint(byte[] publicKey) { + BigInteger x = new BigInteger(1, Arrays.copyOfRange(publicKey, 1, 33)); + BigInteger y = new BigInteger(1, Arrays.copyOfRange(publicKey, 33, 65)); + return EC_CURVE_PARAMETERS.getCurve().createPoint(x, y); + } + + @Override + public boolean isSignatureValid(byte[] data, byte[] signature, Pubkey pubkey) { + try { + ECParameterSpec spec = new ECParameterSpec( + EC_CURVE_PARAMETERS.getCurve(), + EC_CURVE_PARAMETERS.getG(), + EC_CURVE_PARAMETERS.getN(), + EC_CURVE_PARAMETERS.getH(), + EC_CURVE_PARAMETERS.getSeed() + ); + + ECPoint Q = keyToPoint(pubkey.getSigningKey()); + KeySpec keySpec = new ECPublicKeySpec(Q, spec); + PublicKey publicKey = KeyFactory.getInstance("ECDSA", "BC").generatePublic(keySpec); + + Signature sig = Signature.getInstance("ECDSA", "BC"); + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] getSignature(byte[] data, PrivateKey privateKey) { + try { + ECParameterSpec spec = new ECParameterSpec( + EC_CURVE_PARAMETERS.getCurve(), + EC_CURVE_PARAMETERS.getG(), + EC_CURVE_PARAMETERS.getN(), + EC_CURVE_PARAMETERS.getH(), + EC_CURVE_PARAMETERS.getSeed() + ); + + BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); + KeySpec keySpec = new ECPrivateKeySpec(d, spec); + java.security.PrivateKey privKey = KeyFactory.getInstance("ECDSA", "BC").generatePrivate(keySpec); + + Signature sig = Signature.getInstance("ECDSA", "BC"); + sig.initSign(privKey); + sig.update(data); + return sig.sign(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] multiply(byte[] K, byte[] r) { + return keyToPoint(K).multiply(keyToBigInt(r)).normalize().getEncoded(false); + } + + @Override + public byte[] createPoint(byte[] x, byte[] y) { + return EC_CURVE_PARAMETERS.getCurve().createPoint( + new BigInteger(1, x), + new BigInteger(1, y) + ).getEncoded(false); + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java similarity index 54% rename from domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java rename to cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java index 9b97eb1..3a68968 100644 --- a/domain/src/test/java/ch/dissem/bitmessage/utils/SecurityTest.java +++ b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java @@ -1,38 +1,29 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.utils; +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.cryptography.bc.BouncyCryptography; +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 java.security.KeyPairGenerator; 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 10.04.15. + * Created by chris on 19.07.15. */ -public class SecurityTest { +public class CryptographyTest { public static final byte[] TEST_VALUE = "teststring".getBytes(); public static final byte[] TEST_SHA1 = DatatypeConverter.parseHexBinary("" + "b8473b86d4c2072ca9b08bd28e373e8253e865c4"); @@ -42,29 +33,39 @@ public class SecurityTest { public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" + "cd566972b5e50104011a92b59fa8e0b1234851ae"); + private static BouncyCryptography security; + + public CryptographyTest() { + security = new BouncyCryptography(); + 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)); + assertArrayEquals(TEST_RIPEMD160, security.ripemd160(TEST_VALUE)); } @Test public void testSha1() { - assertArrayEquals(TEST_SHA1, Security.sha1(TEST_VALUE)); + assertArrayEquals(TEST_SHA1, security.sha1(TEST_VALUE)); } @Test public void testSha512() { - assertArrayEquals(TEST_SHA512, Security.sha512(TEST_VALUE)); + assertArrayEquals(TEST_SHA512, security.sha512(TEST_VALUE)); } @Test public void testChaining() { - assertArrayEquals(TEST_SHA512, Security.sha512("test".getBytes(), "string".getBytes())); + assertArrayEquals(TEST_SHA512, security.sha512("test".getBytes(), "string".getBytes())); } @Test public void testDoubleHash() { - assertArrayEquals(Security.sha512(TEST_SHA512), Security.doubleSha512(TEST_VALUE)); + assertArrayEquals(security.sha512(TEST_SHA512), security.doubleSha512(TEST_VALUE)); } @Test(expected = IOException.class) @@ -75,25 +76,26 @@ public class SecurityTest { .objectType(0) .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) .build(); - Security.checkProofOfWork(objectMessage, 1000, 1000); + security.checkProofOfWork(objectMessage, 1000, 1000); } @Test - public void testDoProofOfWork() throws IOException { + 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(); - Security.doProofOfWork(objectMessage, new MultiThreadedPOWEngine(), 1000, 1000); - Security.checkProofOfWork(objectMessage, 1000, 1000); + final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); + security.doProofOfWork(objectMessage, 1000, 1000, + new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + waiter.setValue(nonce); + } + }); + objectMessage.setNonce(waiter.waitForValue()); + security.checkProofOfWork(objectMessage, 1000, 1000); } - - @Test - public void testECIES() throws Exception { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECIES", "BC"); -// kpg.initialize(); - kpg.generateKeyPair(); - } -} +} \ No newline at end of file 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/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java b/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java new file mode 100644 index 0000000..c9506fb --- /dev/null +++ b/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java @@ -0,0 +1,153 @@ +/* + * 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.cryptography.sc; + +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.ports.AbstractCryptography; +import org.spongycastle.asn1.x9.X9ECParameters; +import org.spongycastle.crypto.BufferedBlockCipher; +import org.spongycastle.crypto.CipherParameters; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.ec.CustomNamedCurves; +import org.spongycastle.crypto.engines.AESEngine; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.PKCS7Padding; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.spongycastle.jce.spec.ECParameterSpec; +import org.spongycastle.jce.spec.ECPrivateKeySpec; +import org.spongycastle.jce.spec.ECPublicKeySpec; +import org.spongycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.KeySpec; +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 SpongyCryptography extends AbstractCryptography { + private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); + + static { + java.security.Security.addProvider(new BouncyCastleProvider()); + } + + public SpongyCryptography() { + super("SC"); + } + + @Override + public byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector) { + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); + + CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); + + cipher.init(encrypt, params); + + byte[] buffer = new byte[cipher.getOutputSize(data.length)]; + int length = cipher.processBytes(data, 0, data.length, buffer, 0); + try { + length += cipher.doFinal(buffer, length); + } catch (InvalidCipherTextException e) { + throw new IllegalArgumentException(e); + } + if (length < buffer.length) { + return Arrays.copyOfRange(buffer, 0, length); + } + return buffer; + } + + @Override + public byte[] createPublicKey(byte[] privateKey) { + return EC_CURVE_PARAMETERS.getG().multiply(keyToBigInt(privateKey)).normalize().getEncoded(false); + } + + private ECPoint keyToPoint(byte[] publicKey) { + BigInteger x = new BigInteger(1, Arrays.copyOfRange(publicKey, 1, 33)); + BigInteger y = new BigInteger(1, Arrays.copyOfRange(publicKey, 33, 65)); + return EC_CURVE_PARAMETERS.getCurve().createPoint(x, y); + } + + @Override + public boolean isSignatureValid(byte[] data, byte[] signature, Pubkey pubkey) { + try { + ECParameterSpec spec = new ECParameterSpec( + EC_CURVE_PARAMETERS.getCurve(), + EC_CURVE_PARAMETERS.getG(), + EC_CURVE_PARAMETERS.getN(), + EC_CURVE_PARAMETERS.getH(), + EC_CURVE_PARAMETERS.getSeed() + ); + + ECPoint Q = keyToPoint(pubkey.getSigningKey()); + KeySpec keySpec = new ECPublicKeySpec(Q, spec); + PublicKey publicKey = KeyFactory.getInstance("ECDSA", "SC").generatePublic(keySpec); + + Signature sig = Signature.getInstance("ECDSA", "SC"); + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] getSignature(byte[] data, PrivateKey privateKey) { + try { + ECParameterSpec spec = new ECParameterSpec( + EC_CURVE_PARAMETERS.getCurve(), + EC_CURVE_PARAMETERS.getG(), + EC_CURVE_PARAMETERS.getN(), + EC_CURVE_PARAMETERS.getH(), + EC_CURVE_PARAMETERS.getSeed() + ); + + BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); + KeySpec keySpec = new ECPrivateKeySpec(d, spec); + java.security.PrivateKey privKey = KeyFactory.getInstance("ECDSA", "SC").generatePrivate(keySpec); + + Signature sig = Signature.getInstance("ECDSA", "SC"); + sig.initSign(privKey); + sig.update(data); + return sig.sign(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] multiply(byte[] K, byte[] r) { + return keyToPoint(K).multiply(keyToBigInt(r)).normalize().getEncoded(false); + } + + @Override + public byte[] createPoint(byte[] x, byte[] y) { + return EC_CURVE_PARAMETERS.getCurve().createPoint( + new BigInteger(1, x), + new BigInteger(1, y) + ).getEncoded(false); + } +} diff --git a/demo/build.gradle b/demo/build.gradle index 8d6414c..84d5907 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -14,16 +14,20 @@ uploadArchives { } } +sourceCompatibility = 1.8 + 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(':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' } 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 0e03880..26d6652 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java @@ -20,12 +20,15 @@ import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.networking.NetworkNode; +import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.repository.*; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; import java.util.List; import java.util.Scanner; @@ -38,27 +41,32 @@ public class Application { private BitmessageContext ctx; - public Application() { + public Application(String 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)) - .networkHandler(new NetworkNode()) + .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) + .networkHandler(new DefaultNetworkHandler()) + .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); + } + } + }) .build(); - ctx.startup(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); - } - } - }); + if (syncServer == null) { + ctx.startup(); + } scanner = new Scanner(System.in); @@ -70,6 +78,9 @@ 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"); @@ -94,6 +105,9 @@ public class Application { break; case "e": break; + case "y": + ctx.synchronize(InetAddress.getByName(syncServer), syncPort, 120, true); + break; default: System.out.println("Unknown command. Please try again."); } @@ -102,6 +116,7 @@ public class Application { } } while (!"e".equals(command)); LOG.info("Shutting down client"); + ctx.cleanup(); ctx.shutdown(); } 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 4ba8fa7..b0e114b 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java @@ -17,8 +17,10 @@ package ch.dissem.bitmessage.demo; import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.networking.NetworkNode; +import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.repository.*; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.wif.WifExporter; import ch.dissem.bitmessage.wif.WifImporter; import org.kohsuke.args4j.CmdLineException; @@ -49,7 +51,9 @@ public class Main { .inventory(new JdbcInventory(jdbcConfig)) .nodeRegistry(new MemoryNodeRegistry()) .messageRepo(new JdbcMessageRepository(jdbcConfig)) - .networkHandler(new NetworkNode()) + .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) + .networkHandler(new DefaultNetworkHandler()) + .cryptography(new BouncyCryptography()) .port(48444) .build(); @@ -60,7 +64,7 @@ public class Main { new WifImporter(ctx, options.importWIF).importAll(); } } else { - new Application(); + new Application(options.syncServer, options.syncPort); } } @@ -70,5 +74,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/domain/build.gradle b/domain/build.gradle deleted file mode 100644 index eb83499..0000000 --- a/domain/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -uploadArchives { - repositories { - mavenDeployer { - pom.project { - name 'Jabit Domain' - artifactId = 'jabit-domain' - 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.' - } - } - } -} - -dependencies { - compile 'org.slf4j:slf4j-api:1.7.12' - compile 'org.bouncycastle:bcprov-jdk15on:1.52' - testCompile group: 'junit', name: 'junit', version: '4.11' -} \ No newline at end of file diff --git a/domain/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/domain/src/main/java/ch/dissem/bitmessage/BitmessageContext.java deleted file mode 100644 index 7878ada..0000000 --- a/domain/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.utils.Property; -import ch.dissem.bitmessage.utils.Security; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; - -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; - -/** - * <p>Use this class if you want to create a Bitmessage client.</p> - * You'll need the Builder to create a BitmessageContext, and set the following properties: - * <ul> - * <li>addressRepo</li> - * <li>inventory</li> - * <li>nodeRegistry</li> - * <li>networkHandler</li> - * <li>messageRepo</li> - * <li>streams</li> - * </ul> - * <p>The default implementations in the different module builds can be used.</p> - * <p>The port defaults to 8444 (the default Bitmessage port)</p> - */ -public class BitmessageContext { - public static final int CURRENT_VERSION = 3; - private final static Logger LOG = LoggerFactory.getLogger(BitmessageContext.class); - - private final InternalContext ctx; - - private Listener listener; - - private BitmessageContext(Builder builder) { - ctx = new InternalContext(builder); - } - - public AddressRepository addresses() { - return ctx.getAddressRepo(); - } - - public MessageRepository messages() { - return ctx.getMessageRepository(); - } - - public BitmessageAddress createIdentity(boolean shorter, Feature... features) { - BitmessageAddress identity = new BitmessageAddress(new PrivateKey( - shorter, - ctx.getStreams()[0], - ctx.getNetworkNonceTrialsPerByte(), - ctx.getNetworkExtraBytes(), - features - )); - ctx.getAddressRepo().save(identity); - // TODO: this should happen in a separate thread - ctx.sendPubkey(identity, identity.getStream()); - return identity; - } - - public void addDistributedMailingList(String address, String alias) { - // TODO - } - - public void broadcast(BitmessageAddress from, String subject, String message) { - // TODO: all this should happen in a separate thread - 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); - } - - public void send(BitmessageAddress from, BitmessageAddress to, String subject, String message) { - if (from.getPrivateKey() == null) { - throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); - } - // TODO: all this should happen in a separate thread - Plaintext msg = new Plaintext.Builder(MSG) - .from(from) - .to(to) - .message(subject, message) - .build(); - if (to.getPubkey() == null) { - tryToFindMatchingPubkey(to); - } - if (to.getPubkey() == null) { - LOG.info("Public key is missing from recipient. Requesting."); - requestPubkey(from, to); - msg.setStatus(PUBKEY_REQUESTED); - ctx.getMessageRepository().save(msg); - } else { - LOG.info("Sending message."); - msg.setStatus(DOING_PROOF_OF_WORK); - ctx.getMessageRepository().save(msg); - ctx.send( - from, - to, - new Msg(msg), - +2 * DAY, - ctx.getNonceTrialsPerByte(to), - ctx.getExtraBytes(to) - ); - msg.setStatus(SENT); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); - ctx.getMessageRepository().save(msg); - } - } - - private void requestPubkey(BitmessageAddress requestingIdentity, BitmessageAddress address) { - ctx.send( - requestingIdentity, - address, - new GetPubkey(address), - +28 * DAY, - ctx.getNetworkNonceTrialsPerByte(), - ctx.getNetworkExtraBytes() - ); - } - - private void send(long stream, ObjectPayload payload, long timeToLive) { - long expires = UnixTime.now(+timeToLive); - LOG.info("Expires at " + expires); - ObjectMessage object = new ObjectMessage.Builder() - .stream(stream) - .expiresTime(expires) - .payload(payload) - .build(); - Security.doProofOfWork(object, ctx.getProofOfWorkEngine(), - ctx.getNetworkNonceTrialsPerByte(), ctx.getNetworkExtraBytes()); - ctx.getInventory().storeObject(object); - ctx.getNetworkHandler().offer(object.getInventoryVector()); - } - - public void startup(Listener listener) { - this.listener = listener; - ctx.getNetworkHandler().start(new DefaultMessageListener(ctx, listener)); - } - - public void shutdown() { - ctx.getNetworkHandler().stop(); - } - - public void addContact(BitmessageAddress contact) { - ctx.getAddressRepo().save(contact); - tryToFindMatchingPubkey(contact); - if (contact.getPubkey() == null) { - ctx.requestPubkey(contact); - } - } - - private void tryToFindMatchingPubkey(BitmessageAddress address) { - for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) { - try { - Pubkey pubkey = (Pubkey) object.getPayload(); - if (address.getVersion() == 4) { - V4Pubkey v4Pubkey = (V4Pubkey) pubkey; - if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) { - v4Pubkey.decrypt(address.getPublicDecryptionKey()); - if (object.isSignatureValid(v4Pubkey)) { - address.setPubkey(v4Pubkey); - ctx.getAddressRepo().save(address); - break; - } else { - LOG.debug("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); - tryToFindBroadcastsForAddress(address); - } - - private void tryToFindBroadcastsForAddress(BitmessageAddress address) { - for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), Broadcast.getVersion(address), ObjectType.BROADCAST)) { - try { - Broadcast broadcast = (Broadcast) object.getPayload(); - broadcast.decrypt(address); - listener.receive(broadcast.getPlaintext()); - } catch (DecryptionFailedException ignore) { - } catch (Exception e) { - LOG.debug(e.getMessage(), e); - } - } - } - - public Property status() { - return new Property("status", null, - ctx.getNetworkHandler().getNetworkStatus() - ); - } - - public interface Listener { - void receive(Plaintext plaintext); - } - - public static final class Builder { - int port = 8444; - Inventory inventory; - NodeRegistry nodeRegistry; - NetworkHandler networkHandler; - AddressRepository addressRepo; - MessageRepository messageRepo; - ProofOfWorkEngine proofOfWorkEngine; - - public Builder() { - } - - public Builder port(int port) { - this.port = port; - return this; - } - - public Builder inventory(Inventory inventory) { - this.inventory = inventory; - return this; - } - - public Builder nodeRegistry(NodeRegistry nodeRegistry) { - this.nodeRegistry = nodeRegistry; - return this; - } - - public Builder networkHandler(NetworkHandler networkHandler) { - this.networkHandler = networkHandler; - return this; - } - - public Builder addressRepo(AddressRepository addressRepo) { - this.addressRepo = addressRepo; - return this; - } - - public Builder messageRepo(MessageRepository messageRepo) { - this.messageRepo = messageRepo; - return this; - } - - public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) { - this.proofOfWorkEngine = proofOfWorkEngine; - return this; - } - - public BitmessageContext build() { - nonNull("inventory", inventory); - nonNull("nodeRegistry", nodeRegistry); - nonNull("networkHandler", networkHandler); - nonNull("addressRepo", addressRepo); - nonNull("messageRepo", messageRepo); - if (proofOfWorkEngine == null) { - proofOfWorkEngine = new MultiThreadedPOWEngine(); - } - return new BitmessageContext(this); - } - - private void nonNull(String name, Object o) { - if (o == null) throw new IllegalStateException(name + " must not be null"); - } - } - -} diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java deleted file mode 100644 index 25e248e..0000000 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.utils; - -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.crypto.ec.CustomNamedCurves; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jce.spec.ECParameterSpec; -import org.bouncycastle.jce.spec.ECPrivateKeySpec; -import org.bouncycastle.jce.spec.ECPublicKeySpec; -import org.bouncycastle.math.ec.ECPoint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.io.IOException; -import java.math.BigInteger; -import java.security.*; -import java.security.spec.KeySpec; -import java.util.Arrays; - -/** - * Provides some methods to help with hashing and encryption. All randoms are created using {@link SecureRandom}, - * which should be secure enough. - */ -public class Security { - public static final Logger LOG = LoggerFactory.getLogger(Security.class); - private static final SecureRandom RANDOM = new SecureRandom(); - private static final BigInteger TWO = BigInteger.valueOf(2); - private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); - - static { - java.security.Security.addProvider(new BouncyCastleProvider()); - } - - /** - * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at - * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in - * success on the same thread. - * - * @param data to get hashed - * @return SHA-512 hash of data - */ - public static byte[] sha512(byte[]... data) { - return hash("SHA-512", data); - } - - /** - * A helper method to calculate doubleSHA-512 hashes. Please note that a new {@link MessageDigest} object is created - * at each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in - * success on the same thread. - * - * @param data to get hashed - * @return SHA-512 hash of data - */ - public static byte[] doubleSha512(byte[]... data) { - MessageDigest mda = md("SHA-512"); - for (byte[] d : data) { - mda.update(d); - } - return mda.digest(mda.digest()); - } - - /** - * A helper method to calculate double SHA-512 hashes. This method allows to only use a part of the available bytes - * to use for the hash calculation. - * <p> - * Please note that a new {@link MessageDigest} object is created at each call (to ensure thread safety), so you - * shouldn't use this if you need to do many hash calculations in short order on the same thread. - * </p> - * - * @param data to get hashed - * @param length number of bytes to be taken into account - * @return SHA-512 hash of data - */ - public static byte[] doubleSha512(byte[] data, int length) { - MessageDigest mda = md("SHA-512"); - mda.update(data, 0, length); - return mda.digest(mda.digest()); - } - - - /** - * A helper method to calculate RIPEMD-160 hashes. Supplying multiple byte arrays has the same result as a - * concatenation of all arrays, but might perform better. - * <p> - * Please note that a new {@link MessageDigest} object is created at - * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short - * order on the same thread. - * </p> - * - * @param data to get hashed - * @return RIPEMD-160 hash of data - */ - public static byte[] ripemd160(byte[]... data) { - return hash("RIPEMD160", data); - } - - /** - * A helper method to calculate double SHA-256 hashes. This method allows to only use a part of the available bytes - * to use for the hash calculation. - * <p> - * Please note that a new {@link MessageDigest} object is created at - * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short - * order on the same thread. - * </p> - * - * @param data to get hashed - * @param length number of bytes to be taken into account - * @return SHA-256 hash of data - */ - public static byte[] doubleSha256(byte[] data, int length) { - MessageDigest mda = md("SHA-256"); - mda.update(data, 0, length); - return mda.digest(mda.digest()); - } - - /** - * A helper method to calculate SHA-1 hashes. Supplying multiple byte arrays has the same result as a - * concatenation of all arrays, but might perform better. - * <p> - * Please note that a new {@link MessageDigest} object is created at - * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short - * order on the same thread. - * </p> - * - * @param data to get hashed - * @return SHA hash of data - */ - public static byte[] sha1(byte[]... data) { - return hash("SHA-1", data); - } - - /** - * @param length number of bytes to return - * @return an array of the given size containing random bytes - */ - public static byte[] randomBytes(int length) { - byte[] result = new byte[length]; - RANDOM.nextBytes(result); - return result; - } - - /** - * Calculates the proof of work. This might take a long time, depending on the hardware, message size and time to - * live. - * - * @param object to do the proof of work for - * @param worker doing the actual proof of work - * @param nonceTrialsPerByte difficulty - * @param extraBytes bytes to add to the object size (makes it more difficult to send small messages) - */ - public static void doProofOfWork(ObjectMessage object, ProofOfWorkEngine worker, long nonceTrialsPerByte, - long extraBytes) { - try { - if (nonceTrialsPerByte < 1000) nonceTrialsPerByte = 1000; - if (extraBytes < 1000) extraBytes = 1000; - - byte[] initialHash = getInitialHash(object); - - byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); - - byte[] nonce = worker.calculateNonce(initialHash, target); - object.setNonce(nonce); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * @param object to be checked - * @param nonceTrialsPerByte difficulty - * @param extraBytes bytes to add to the object size - * @throws InsufficientProofOfWorkException if proof of work doesn't check out (makes it more difficult to send small messages) - */ - public static void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) - throws IOException { - byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes); - byte[] value = Security.doubleSha512(object.getNonce(), getInitialHash(object)); - if (Bytes.lt(target, value, 8)) { - throw new InsufficientProofOfWorkException(target, value); - } - } - - private static byte[] getInitialHash(ObjectMessage object) throws IOException { - return Security.sha512(object.getPayloadBytesWithoutNonce()); - } - - private static byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException { - BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now()); - LOG.debug("TTL: " + TTL + "s"); - 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)))); - return Bytes.expand(numerator.divide(denominator).toByteArray(), 8); - } - - private static byte[] hash(String algorithm, byte[]... data) { - MessageDigest mda = md(algorithm); - for (byte[] d : data) { - mda.update(d); - } - return mda.digest(); - } - - private static MessageDigest md(String algorithm) { - try { - return MessageDigest.getInstance(algorithm, "BC"); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - - /** - * Calculates the MAC for a message (data) - * - * @param key_m the symmetric key used - * @param data the message data to calculate the MAC for - * @return the MAC - */ - public static byte[] mac(byte[] key_m, byte[] data) { - try { - Mac mac = Mac.getInstance("HmacSHA256", "BC"); - mac.init(new SecretKeySpec(key_m, "HmacSHA256")); - return mac.doFinal(data); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - - /** - * Create a new public key fom given private keys. - * - * @param version of the public key / address - * @param stream of the address - * @param privateSigningKey private key used for signing - * @param privateEncryptionKey private key used for encryption - * @param nonceTrialsPerByte proof of work difficulty - * @param extraBytes bytes to add for the proof of work (make it harder for small messages) - * @param features of the address - * @return a public key object - */ - public static Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey, - long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { - return Factory.createPubkey(version, stream, - createPublicKey(privateSigningKey).getEncoded(false), - createPublicKey(privateEncryptionKey).getEncoded(false), - nonceTrialsPerByte, extraBytes, features); - } - - /** - * @param privateKey private key as byte array - * @return a public key corresponding to the given private key - */ - public static ECPoint createPublicKey(byte[] privateKey) { - return EC_CURVE_PARAMETERS.getG().multiply(keyToBigInt(privateKey)).normalize(); - } - - /** - * @param privateKey private key as byte array - * @return a big integer representation (unsigned) of the given bytes - */ - public static BigInteger keyToBigInt(byte[] privateKey) { - return new BigInteger(1, privateKey); - } - - public static ECPoint keyToPoint(byte[] publicKey) { - BigInteger x = new BigInteger(1, Arrays.copyOfRange(publicKey, 1, 33)); - BigInteger y = new BigInteger(1, Arrays.copyOfRange(publicKey, 33, 65)); - return EC_CURVE_PARAMETERS.getCurve().createPoint(x, y); - } - - public static ECPoint createPoint(byte[] x, byte[] y) { - return EC_CURVE_PARAMETERS.getCurve().createPoint( - new BigInteger(1, x), - new BigInteger(1, y) - ); - } - - /** - * @param data to check - * @param signature the signature of the message - * @param pubkey the sender's public key - * @return true if the signature is valid, false otherwise - */ - public static boolean isSignatureValid(byte[] data, byte[] signature, Pubkey pubkey) { - try { - ECParameterSpec spec = new ECParameterSpec( - EC_CURVE_PARAMETERS.getCurve(), - EC_CURVE_PARAMETERS.getG(), - EC_CURVE_PARAMETERS.getN(), - EC_CURVE_PARAMETERS.getH(), - EC_CURVE_PARAMETERS.getSeed() - ); - - ECPoint Q = keyToPoint(pubkey.getSigningKey()); - KeySpec keySpec = new ECPublicKeySpec(Q, spec); - PublicKey publicKey = KeyFactory.getInstance("ECDSA", "BC").generatePublic(keySpec); - - Signature sig = Signature.getInstance("ECDSA", "BC"); - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Calculate the signature of data, using the given private key. - * - * @param data to be signed - * @param privateKey to be used for signing - * @return the signature - */ - public static byte[] getSignature(byte[] data, ch.dissem.bitmessage.entity.valueobject.PrivateKey privateKey) { - try { - ECParameterSpec spec = new ECParameterSpec( - EC_CURVE_PARAMETERS.getCurve(), - EC_CURVE_PARAMETERS.getG(), - EC_CURVE_PARAMETERS.getN(), - EC_CURVE_PARAMETERS.getH(), - EC_CURVE_PARAMETERS.getSeed() - ); - - BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); - KeySpec keySpec = new ECPrivateKeySpec(d, spec); - PrivateKey privKey = KeyFactory.getInstance("ECDSA", "BC").generatePrivate(keySpec); - - Signature sig = Signature.getInstance("ECDSA", "BC"); - sig.initSign(privKey); - sig.update(data); - return sig.sign(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * @return a random number of type long - */ - public static long randomNonce() { - return RANDOM.nextLong(); - } -} diff --git a/domain/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java b/domain/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java deleted file mode 100644 index aed5722..0000000 --- a/domain/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.ports; - -import ch.dissem.bitmessage.utils.Bytes; -import ch.dissem.bitmessage.utils.Security; -import org.junit.Test; - -import static org.junit.Assert.assertTrue; - -/** - * Created by chris on 17.04.15. - */ -public class ProofOfWorkEngineTest { - @Test - public void testSimplePOWEngine() { - testPOW(new SimplePOWEngine()); - } - - @Test - public void testThreadedPOWEngine() { - testPOW(new MultiThreadedPOWEngine()); - } - - private void testPOW(ProofOfWorkEngine engine) { - long time = System.currentTimeMillis(); - byte[] initialHash = Security.sha512(new byte[]{1, 3, 6, 4}); - byte[] target = {0, 0, -1, -1, -1, -1, -1, -1}; - - byte[] nonce = engine.calculateNonce(initialHash, target); - System.out.println("Calculating nonce took " + (System.currentTimeMillis() - time) + "ms"); - assertTrue(Bytes.lt(Security.doubleSha512(nonce, initialHash), target, 8)); - } -} 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..49c6f1b --- /dev/null +++ b/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java @@ -0,0 +1,143 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.extensions; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.entity.payload.CryptoBox; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.*; + +import static ch.dissem.bitmessage.utils.Decode.*; +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * A {@link CustomMessage} implementation that contains signed and encrypted data. + * + * @author Christian Basler + */ +public class CryptoCustomMessage<T extends Streamable> extends CustomMessage { + public static final String COMMAND = "ENCRYPTED"; + private final Reader<T> dataReader; + private CryptoBox container; + private BitmessageAddress sender; + private T data; + + public CryptoCustomMessage(T data) throws IOException { + super(COMMAND); + this.data = data; + this.dataReader = null; + } + + private CryptoCustomMessage(CryptoBox container, Reader<T> dataReader) { + super(COMMAND); + this.container = container; + this.dataReader = dataReader; + } + + public static <T extends Streamable> CryptoCustomMessage<T> read(CustomMessage data, Reader<T> dataReader) throws IOException { + CryptoBox cryptoBox = CryptoBox.read(new ByteArrayInputStream(data.getData()), data.getData().length); + return new CryptoCustomMessage<>(cryptoBox, dataReader); + } + + public BitmessageAddress getSender() { + return sender; + } + + public void signAndEncrypt(BitmessageAddress identity, byte[] publicKey) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + Encode.varInt(identity.getVersion(), out); + Encode.varInt(identity.getStream(), out); + Encode.int32(identity.getPubkey().getBehaviorBitfield(), out); + out.write(identity.getPubkey().getSigningKey(), 1, 64); + out.write(identity.getPubkey().getEncryptionKey(), 1, 64); + if (identity.getVersion() >= 3) { + Encode.varInt(identity.getPubkey().getNonceTrialsPerByte(), out); + Encode.varInt(identity.getPubkey().getExtraBytes(), out); + } + + data.write(out); + Encode.varBytes(security().getSignature(out.toByteArray(), identity.getPrivateKey()), out); + container = new CryptoBox(out.toByteArray(), publicKey); + } + + public T decrypt(byte[] privateKey) throws IOException, DecryptionFailedException { + SignatureCheckingInputStream in = new SignatureCheckingInputStream(container.decrypt(privateKey)); + + long addressVersion = varInt(in); + long stream = varInt(in); + int behaviorBitfield = int32(in); + byte[] publicSigningKey = bytes(in, 64); + byte[] publicEncryptionKey = bytes(in, 64); + long nonceTrialsPerByte = addressVersion >= 3 ? varInt(in) : 0; + long extraBytes = addressVersion >= 3 ? varInt(in) : 0; + + sender = new BitmessageAddress(Factory.createPubkey( + addressVersion, + stream, + publicSigningKey, + publicEncryptionKey, + nonceTrialsPerByte, + extraBytes, + behaviorBitfield + )); + + data = dataReader.read(sender, in); + + in.checkSignature(sender.getPubkey()); + + return data; + } + + @Override + public void write(OutputStream out) throws IOException { + Encode.varString(COMMAND, out); + container.write(out); + } + + public interface Reader<T> { + T read(BitmessageAddress sender, InputStream in) throws IOException; + } + + private class SignatureCheckingInputStream extends InputStream { + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final InputStream wrapped; + + private SignatureCheckingInputStream(InputStream wrapped) { + this.wrapped = wrapped; + } + + @Override + public int read() throws IOException { + int read = wrapped.read(); + if (read >= 0) out.write(read); + return read; + } + + public void checkSignature(Pubkey pubkey) throws IOException, RuntimeException { + if (!security().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { + throw new RuntimeException("Signature check failed"); + } + } + } +} 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..0024aaa --- /dev/null +++ b/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.extensions.pow; + +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.extensions.CryptoCustomMessage; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +import static ch.dissem.bitmessage.utils.Decode.*; + +/** + * @author Christian Basler + */ +public class ProofOfWorkRequest implements Streamable { + private final BitmessageAddress sender; + private final byte[] initialHash; + private final Request request; + + private final byte[] data; + + public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request) { + this(sender, initialHash, request, new byte[0]); + } + + public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request, byte[] data) { + this.sender = sender; + this.initialHash = initialHash; + this.request = request; + this.data = data; + } + + public static ProofOfWorkRequest read(BitmessageAddress client, InputStream in) throws IOException { + return new ProofOfWorkRequest( + client, + bytes(in, 64), + Request.valueOf(varString(in)), + varBytes(in) + ); + } + + public BitmessageAddress getSender() { + return sender; + } + + public byte[] getInitialHash() { + return initialHash; + } + + public Request getRequest() { + return request; + } + + public byte[] getData() { + return data; + } + + @Override + public void write(OutputStream out) throws IOException { + out.write(initialHash); + Encode.varString(request.name(), out); + Encode.varBytes(data, out); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ProofOfWorkRequest other = (ProofOfWorkRequest) o; + + if (!sender.equals(other.sender)) return false; + if (!Arrays.equals(initialHash, other.initialHash)) return false; + if (request != other.request) return false; + return Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + int result = sender.hashCode(); + result = 31 * result + Arrays.hashCode(initialHash); + result = 31 * result + request.hashCode(); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + public static class Reader implements CryptoCustomMessage.Reader<ProofOfWorkRequest> { + private final BitmessageAddress identity; + + public Reader(BitmessageAddress identity) { + this.identity = identity; + } + + @Override + public ProofOfWorkRequest read(BitmessageAddress sender, InputStream in) throws IOException { + return ProofOfWorkRequest.read(identity, in); + } + } + + public enum Request { + CALCULATE, + CALCULATING, + COMPLETE + } +} 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 new file mode 100644 index 0000000..eb6ed30 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +# Don't change this file - override those properties in the +# gradle.properties file in your home directory instead + +signing.keyId= +signing.password= +#signing.secretKeyRingFile= + +ossrhUsername= +ossrhPassword= + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f2e98f0..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.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip diff --git a/networking/build.gradle b/networking/build.gradle index 07d268e..984f585 100644 --- a/networking/build.gradle +++ b/networking/build.gradle @@ -11,7 +11,10 @@ uploadArchives { } dependencies { - compile project(':domain') - testCompile 'org.slf4j:slf4j-simple:1.7.12' + 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') } \ 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 0eb0f09..54bfee4 100644 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java @@ -25,8 +25,6 @@ import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.NetworkHandler.MessageListener; -import ch.dissem.bitmessage.utils.DebugUtils; -import ch.dissem.bitmessage.utils.Security; import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +32,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; @@ -43,54 +42,87 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; 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; /** * A connection to a specific node */ -public class Connection implements Runnable { +class Connection { public static final int READ_TIMEOUT = 2000; - private final static Logger LOG = LoggerFactory.getLogger(Connection.class); + private static final Logger LOG = LoggerFactory.getLogger(Connection.class); private static final int CONNECT_TIMEOUT = 5000; + + private final long startTime; private final ConcurrentMap<InventoryVector, Long> ivCache; - private InternalContext ctx; - private Mode mode; - private State state; - private Socket socket; + private final InternalContext ctx; + private final Mode mode; + private final Socket socket; + private final MessageListener listener; + private final NetworkAddress host; + private final NetworkAddress node; + private final Queue<MessagePayload> sendingQueue = new ConcurrentLinkedDeque<>(); + 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; private OutputStream out; - private MessageListener listener; private int version; private long[] streams; - private NetworkAddress host; - private NetworkAddress node; - private Queue<MessagePayload> sendingQueue = new ConcurrentLinkedDeque<>(); - private ConcurrentMap<InventoryVector, Long> requestedObjects; + 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 { - this(context, mode, listener, requestedObjectsMap); - this.socket = socket; - this.node = new NetworkAddress.Builder().ip(socket.getInetAddress()).port(socket.getPort()).stream(1).build(); + 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) { - this(context, mode, listener, requestedObjectsMap); - this.socket = new Socket(); - this.node = node; + 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, - ConcurrentMap<InventoryVector, Long> requestedObjectsMap) { + private Connection(InternalContext context, Mode mode, MessageListener listener, Socket socket, + 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.requestedObjects = requestedObjectsMap; + this.socket = socket; + 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(); - ivCache = new ConcurrentHashMap<>(); + 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.SYNC, listener, new Socket(address, port), + new HashSet<InventoryVector>(), + new HashSet<InventoryVector>(), + new NetworkAddress.Builder().ip(address).port(port).stream(1).build(), + timeoutInSeconds); + } + + public long getStartTime() { + return startTime; } public Mode getMode() { @@ -105,101 +137,42 @@ public class Connection implements Runnable { return node; } - @Override - public void run() { - try (Socket socket = this.socket) { - if (!socket.isConnected()) { - LOG.debug("Trying to connect to node " + node); - socket.connect(new InetSocketAddress(node.toInetAddress(), node.getPort()), CONNECT_TIMEOUT); - } - socket.setSoTimeout(READ_TIMEOUT); - this.in = socket.getInputStream(); - this.out = socket.getOutputStream(); - if (mode == CLIENT) { - send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); - } - while (state != DISCONNECTED) { - try { - NetworkMessage msg = Factory.getNetworkMessage(version, in); - if (msg == null) - continue; - switch (state) { - case ACTIVE: - receiveMessage(msg.getPayload()); - sendQueue(); - break; + @SuppressWarnings("RedundantIfStatement") + private boolean syncFinished(NetworkMessage msg) { + if (mode != SYNC) { + return false; + } + if (Thread.interrupted()) { + return true; + } + if (state != ACTIVE) { + return false; + } + if (syncTimeout < UnixTime.now()) { + LOG.info("Synchronization timed out"); + return true; + } + if (msg == null) { + if (requestedObjects.isEmpty() && sendingQueue.isEmpty()) + return true; - 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) { - this.version = payload.getVersion(); - this.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()) state = DISCONNECTED; - } catch (SocketTimeoutException ignore) { - if (state == ACTIVE) { - sendQueue(); - } - } - } - } catch (IOException | NodeException e) { - disconnect(); - LOG.debug("Disconnected from node " + node + ": " + e.getMessage()); - } catch (RuntimeException e) { - disconnect(); - throw e; + readTimeoutCounter++; + return readTimeoutCounter > 1; + } else { + readTimeoutCounter = 0; + 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(Arrays.asList(node)); - } - - private void sendQueue() { - if (sendingQueue.size() > 0) { - LOG.debug("Sending " + sendingQueue.size() + " messages to node " + node); - } - for (MessagePayload msg = sendingQueue.poll(); msg != null; msg = sendingQueue.poll()) { - send(msg); - } } private void cleanupIvCache() { @@ -227,41 +200,16 @@ public class Connection implements Runnable { } } - 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; + int originalSize = inv.getInventory().size(); 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.removeAll(commonRequestedObjects); + LOG.debug("Received inventory with " + originalSize + " elements, of which are " + missing.size() + " missing."); - updateRequestedObjects(missing); send(new GetData.Builder().inventory(missing).build()); break; case GETDATA: @@ -274,21 +222,26 @@ public class Connection implements Runnable { case OBJECT: ObjectMessage objectMessage = (ObjectMessage) messagePayload; try { - LOG.debug("Received object " + objectMessage.getInventoryVector()); - Security.checkProofOfWork(objectMessage, ctx.getNetworkNonceTrialsPerByte(), ctx.getNetworkExtraBytes()); + requestedObjects.remove(objectMessage.getInventoryVector()); + if (ctx.getInventory().contains(objectMessage)) { + LOG.trace("Received object " + objectMessage.getInventoryVector() + " - already in inventory"); + break; + } listener.receive(objectMessage); + security().checkProofOfWork(objectMessage, ctx.getNetworkNonceTrialsPerByte(), ctx.getNetworkExtraBytes()); 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()); + 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); - DebugUtils.saveToFile(objectMessage); } finally { - requestedObjects.remove(objectMessage.getInventoryVector()); + if (commonRequestedObjects.remove(objectMessage.getInventoryVector())) { + LOG.debug("Received object that wasn't requested."); + } } break; case ADDR: @@ -296,6 +249,7 @@ public class Connection implements Runnable { LOG.debug("Received " + addr.getAddresses().size() + " addresses."); ctx.getNodeRegistry().offerAddresses(addr.getAddresses()); break; + case CUSTOM: case VERACK: case VERSION: throw new RuntimeException("Unexpectedly received '" + messagePayload.getCommand() + "' command"); @@ -318,11 +272,19 @@ public class Connection implements Runnable { 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(); @@ -330,7 +292,6 @@ public class Connection implements Runnable { } public void offer(InventoryVector iv) { - LOG.debug("Offering " + iv + " to node " + node.toString()); sendingQueue.offer(new Inv.Builder() .addInventoryVector(iv) .build()); @@ -354,14 +315,147 @@ public class Connection implements Runnable { return Objects.hash(node); } - public void request(InventoryVector key) { - sendingQueue.offer(new GetData.Builder() - .addInventoryVector(key) - .build() - ); + private synchronized void initSocket(Socket socket) throws IOException { + if (!socketInitialized) { + if (!socket.isConnected()) { + LOG.trace("Trying to connect to node " + node); + socket.connect(new InetSocketAddress(node.toInetAddress(), node.getPort()), CONNECT_TIMEOUT); + } + socket.setSoTimeout(READ_TIMEOUT); + in = socket.getInputStream(); + out = socket.getOutputStream(); + socketInitialized = true; + } } - public enum Mode {SERVER, CLIENT} + public ReaderRunnable getReader() { + return reader; + } + + public WriterRunnable getWriter() { + return writer; + } + + 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 || mode == SYNC) { + send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); + } + while (state != DISCONNECTED) { + if (mode != SYNC) { + if (state == ACTIVE && requestedObjects.isEmpty() && sendingQueue.isEmpty()) { + Thread.sleep(1000); + } else { + 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: + case SYNC: + activateConnection(); + break; + } + } else { + LOG.info("Received unsupported version " + payload.getVersion() + ", disconnecting."); + disconnect(); + } + break; + case VERACK: + switch (mode) { + case SERVER: + activateConnection(); + break; + case CLIENT: + case SYNC: + // NO OP + break; + } + break; + case CUSTOM: + MessagePayload response = ctx.getCustomCommandHandler().handle((CustomMessage) msg.getPayload()); + if (response != null) { + send(response); + } + disconnect(); + break; + default: + throw new NodeException("Command 'version' or 'verack' expected, but was '" + + msg.getPayload().getCommand() + "'"); + } + } + if (socket.isClosed() || syncFinished(msg) || checkOpenRequests()) disconnect(); + } catch (SocketTimeoutException ignore) { + if (state == ACTIVE) { + if (syncFinished(null)) disconnect(); + } + } + } + } catch (InterruptedException | IOException | NodeException 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 { + socket.close(); + } catch (Exception e) { + LOG.debug(e.getMessage(), e); + } + } + } + } + + private boolean checkOpenRequests() { + return !requestedObjects.isEmpty() && lastObjectTime > 0 && (UnixTime.now() - lastObjectTime) > 2 * MINUTE; + } + + public class WriterRunnable implements Runnable { + @Override + public void run() { + try (Socket socket = Connection.this.socket) { + initSocket(socket); + while (state != DISCONNECTED) { + if (!sendingQueue.isEmpty()) { + send(sendingQueue.poll()); + } else { + Thread.sleep(1000); + } + } + } catch (IOException | InterruptedException e) { + LOG.trace("Writer disconnected from node " + node + ": " + e.getMessage()); + disconnect(); + } + } + } } diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java new file mode 100644 index 0000000..d3bec17 --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java @@ -0,0 +1,353 @@ +/* + * 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.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.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.utils.DebugUtils.inc; +import static java.util.Collections.newSetFromMap; + +/** + * Handles all the networky stuff. + */ +public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { + private final static Logger LOG = LoggerFactory.getLogger(DefaultNetworkHandler.class); + + public final static int NETWORK_MAGIC_NUMBER = 8; + + private final Collection<Connection> connections = new ConcurrentLinkedQueue<>(); + private final ExecutorService pool; + private InternalContext ctx; + private ServerSocket serverSocket; + private volatile boolean running; + + private Set<InventoryVector> requestedObjects = newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(50_000)); + + public DefaultNetworkHandler() { + pool = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + } + }); + } + + @Override + public void setContext(InternalContext context) { + this.ctx = context; + } + + @Override + public Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds) { + try { + 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); + } + } + + @Override + public CustomMessage send(InetAddress server, int port, CustomMessage request) { + try (Socket socket = new Socket(server, port)) { + socket.setSoTimeout(Connection.READ_TIMEOUT); + new NetworkMessage(request).write(socket.getOutputStream()); + NetworkMessage networkMessage = Factory.getNetworkMessage(3, socket.getInputStream()); + if (networkMessage != null && networkMessage.getPayload() instanceof CustomMessage) { + return (CustomMessage) networkMessage.getPayload(); + } else { + if (networkMessage == null) { + throw new NodeException("No response from node " + server); + } else { + throw new NodeException("Unexpected response from node " + + server + ": " + networkMessage.getPayload().getCommand()); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void start(final MessageListener listener) { + if (listener == null) { + throw new IllegalStateException("Listener must be set at start"); + } + if (running) { + throw new IllegalStateException("Network already running - you need to stop first."); + } + 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() { + public Connection initialConnection; + + @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; + } + } + boolean forcedDisconnect = false; + for (Iterator<Connection> iterator = 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; + } + } + } + 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, requestedObjects); + if (first) { + initialConnection = c; + first = false; + } + startConnection(c); + } + Thread.sleep(10000); + } else if (initialConnection != null) { + initialConnection.disconnect(); + initialConnection = null; + 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; + } + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public void stop() { + running = false; + try { + serverSocket.close(); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); + } + synchronized (connections) { + for (Connection c : connections) { + c.disconnect(); + } + } + requestedObjects.clear(); + } + + private void startConnection(Connection c) { + synchronized (connections) { + // prevent connecting twice to the same node + if (connections.contains(c)) { + return; + } + connections.add(c); + } + pool.execute(c.getReader()); + pool.execute(c.getWriter()); + } + + @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); + } + } + } + List<Connection> randomSubset = Collections.selectRandom(NETWORK_MAGIC_NUMBER, target); + for (Connection connection : randomSubset) { + connection.offer(iv); + } + } + + @Override + public Property getNetworkStatus() { + TreeSet<Long> streams = new TreeSet<>(); + 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); + } + } + } + } + Property[] streamProperties = new Property[streams.size()]; + int i = 0; + for (Long stream : streams) { + int incoming = incomingConnections.containsKey(stream) ? incomingConnections.get(stream) : 0; + int outgoing = outgoingConnections.containsKey(stream) ? outgoingConnections.get(stream) : 0; + streamProperties[i] = new Property("stream " + stream, + null, new Property("nodes", incoming + outgoing), + new Property("incoming", incoming), + new Property("outgoing", outgoing) + ); + i++; + } + return new Property("network", null, + new Property("connectionManager", running ? "running" : "stopped"), + new Property("connections", null, streamProperties), + new Property("requestedObjects", requestedObjects.size()) + ); + } + + void request(Set<InventoryVector> inventoryVectors) { + if (!running || inventoryVectors.isEmpty()) return; + synchronized (connections) { + 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(); + InventoryVector next; + if (iterator.hasNext()) { + next = iterator.next(); + } else { + return; + } + boolean firstRound = true; + while (firstRound || iterator.hasNext()) { + if (!firstRound) { + next = iterator.next(); + firstRound = true; + } else { + firstRound = false; + } + for (Connection connection : distribution.keySet()) { + if (connection.knowsOf(next)) { + List<InventoryVector> ivs = distribution.get(connection); + if (ivs.size() == 50_000) { + connection.send(new GetData.Builder().inventory(ivs).build()); + ivs.clear(); + } + ivs.add(next); + iterator.remove(); + + if (iterator.hasNext()) { + next = iterator.next(); + firstRound = true; + } else { + firstRound = false; + break; + } + } + } + } + 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/NetworkNode.java b/networking/src/main/java/ch/dissem/bitmessage/networking/NetworkNode.java deleted file mode 100644 index f27f7ff..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/NetworkNode.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.InternalContext.ContextHolder; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.ports.NetworkHandler; -import ch.dissem.bitmessage.utils.Collections; -import ch.dissem.bitmessage.utils.Property; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -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; - -/** - * Handles all the networky stuff. - */ -public class NetworkNode implements NetworkHandler, ContextHolder { - public final static int NETWORK_MAGIC_NUMBER = 8; - private final static Logger LOG = LoggerFactory.getLogger(NetworkNode.class); - private final ExecutorService pool; - private final List<Connection> connections = new LinkedList<>(); - private InternalContext ctx; - private ServerSocket serverSocket; - private Thread serverThread; - private Thread connectionManager; - - private ConcurrentMap<InventoryVector, Long> requestedObjects = new ConcurrentHashMap<>(); - - public NetworkNode() { - pool = Executors.newCachedThreadPool(); - } - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } - - @Override - public void start(final MessageListener listener) { - if (listener == null) { - throw new IllegalStateException("Listener must be set at start"); - } - try { - serverSocket = new ServerSocket(ctx.getPort()); - serverThread = new Thread(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); - } - } - } - }, "server"); - serverThread.start(); - connectionManager = new Thread(new Runnable() { - @Override - public void run() { - while (!Thread.interrupted()) { - try { - int active = 0; - synchronized (connections) { - for (Iterator<Connection> iterator = connections.iterator(); iterator.hasNext(); ) { - Connection c = iterator.next(); - 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(30000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (Exception e) { - LOG.error("Error in connection manager. Ignored.", e); - } - } - LOG.debug("Connection manager shutting down."); - } - }, "connection-manager"); - connectionManager.start(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void stop() { - connectionManager.interrupt(); - try { - serverSocket.close(); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - pool.shutdown(); - synchronized (connections) { - for (Connection c : connections) { - c.disconnect(); - } - } - } - - private void startConnection(Connection c) { - synchronized (connections) { - // prevent connecting twice to the same node - if (connections.contains(c)) { - return; - } - connections.add(c); - } - pool.execute(c); - } - - @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); - } - } - } - LOG.debug(target.size() + " connections available to offer " + iv); - List<Connection> randomSubset = Collections.selectRandom(NETWORK_MAGIC_NUMBER, target); - for (Connection connection : randomSubset) { - connection.offer(iv); - } - } - - @Override - public Property getNetworkStatus() { - TreeSet<Long> streams = new TreeSet<>(); - 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); - } - } - } - } - Property[] streamProperties = new Property[streams.size()]; - int i = 0; - for (Long stream : streams) { - int incoming = incomingConnections.containsKey(stream) ? incomingConnections.get(stream) : 0; - int outgoing = outgoingConnections.containsKey(stream) ? outgoingConnections.get(stream) : 0; - streamProperties[i] = new Property("stream " + stream, - null, new Property("nodes", incoming + outgoing), - new Property("incoming", incoming), - new Property("outgoing", outgoing) - ); - i++; - } - return new Property("network", null, - new Property("connectionManager", connectionManager.isAlive() ? "running" : "stopped"), - new Property("connections", null, streamProperties) - ); - } -} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java new file mode 100644 index 0000000..a45ec47 --- /dev/null +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java @@ -0,0 +1,172 @@ +/* + * 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.networking; + +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.AddressRepository; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.ports.NetworkHandler; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.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; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * FIXME: there really should be sensible tests for the network handler + */ +public class NetworkHandlerTest { + private static NetworkAddress localhost = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(6001).build(); + + private static TestInventory peerInventory; + private static TestInventory nodeInventory; + + private static BitmessageContext peer; + private static BitmessageContext node; + private static NetworkHandler networkHandler; + + @BeforeClass + public static void setUp() { + peerInventory = new TestInventory(); + peer = new BitmessageContext.Builder() + .addressRepo(Mockito.mock(AddressRepository.class)) + .inventory(peerInventory) + .messageRepo(Mockito.mock(MessageRepository.class)) + .powRepo(Mockito.mock(ProofOfWorkRepository.class)) + .port(6001) + .nodeRegistry(new TestNodeRegistry()) + .networkHandler(new DefaultNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(Mockito.mock(BitmessageContext.Listener.class)) + .build(); + peer.startup(); + + nodeInventory = new TestInventory(); + networkHandler = new DefaultNetworkHandler(); + node = new BitmessageContext.Builder() + .addressRepo(Mockito.mock(AddressRepository.class)) + .inventory(nodeInventory) + .messageRepo(Mockito.mock(MessageRepository.class)) + .powRepo(Mockito.mock(ProofOfWorkRepository.class)) + .port(6002) + .nodeRegistry(new TestNodeRegistry(localhost)) + .networkHandler(networkHandler) + .cryptography(new BouncyCryptography()) + .listener(Mockito.mock(BitmessageContext.Listener.class)) + .build(); + } + + @AfterClass + public static void cleanUp() { + shutdown(peer); + } + + private static void shutdown(BitmessageContext node) { + node.shutdown(); + do { + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + } + } while (node.isRunning()); + } + + @Test(timeout = 5_000) + public void ensureNodesAreConnecting() { + try { + node.startup(); + Property status; + do { + Thread.yield(); + status = node.status().getProperty("network", "connections", "stream 0"); + } while (status == null); + assertEquals(1, status.getProperty("outgoing").getValue()); + } finally { + shutdown(node); + } + } + + @Test(timeout = 5_000) + public void ensureObjectsAreSynchronizedIfBothHaveObjects() throws Exception { + peerInventory.init( + "V4Pubkey.payload", + "V5Broadcast.payload" + ); + + nodeInventory.init( + "V1Msg.payload", + "V4Pubkey.payload" + ); + + Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, + mock(NetworkHandler.MessageListener.class), + 10); + future.get(); + assertInventorySize(3, nodeInventory); + assertInventorySize(3, peerInventory); + } + + @Test(timeout = 5_000) + public void ensureObjectsAreSynchronizedIfOnlyPeerHasObjects() throws Exception { + peerInventory.init( + "V4Pubkey.payload", + "V5Broadcast.payload" + ); + + nodeInventory.init(); + + Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, + mock(NetworkHandler.MessageListener.class), + 10); + future.get(); + assertInventorySize(2, nodeInventory); + assertInventorySize(2, peerInventory); + } + + @Test(timeout = 5_000) + public void ensureObjectsAreSynchronizedIfOnlyNodeHasObjects() throws Exception { + peerInventory.init(); + + nodeInventory.init( + "V1Msg.payload" + ); + + Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, + mock(NetworkHandler.MessageListener.class), + 10); + future.get(); + assertInventorySize(1, nodeInventory); + assertInventorySize(1, peerInventory); + } + + private void assertInventorySize(int expected, TestInventory inventory) throws InterruptedException { + long timeout = System.currentTimeMillis() + 1000; + while (expected != inventory.getInventory().size() && System.currentTimeMillis() < timeout) { + Thread.sleep(10); + } + assertEquals(expected, inventory.getInventory().size()); + } +} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java deleted file mode 100644 index 6819311..0000000 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking; - -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.entity.Version; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.utils.UnixTime; -import org.junit.Ignore; -import org.junit.Test; - -/** - * Created by chris on 20.03.15. - */ -public class NetworkNodeTest { - private NetworkAddress localhost = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(8444).build(); - - @Ignore - @Test(expected = InterruptedException.class) - public void testSendMessage() throws Exception { - final Thread baseThread = Thread.currentThread(); - NetworkNode net = new NetworkNode(); -// net.setListener(localhost, new NetworkHandler.MessageListener() { -// @Override -// public void receive(ObjectPayload payload) { -// System.out.println(payload); -// baseThread.interrupt(); -// } -// }); - NetworkMessage ver = new NetworkMessage( - new Version.Builder() - .version(3) - .services(1) - .timestamp(UnixTime.now()) - .addrFrom(localhost) - .addrRecv(localhost) - .nonce(-1) - .userAgent("Test") - .streams(1, 2) - .build() - ); -// net.send(localhost, ver); - Thread.sleep(20000); - } -} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/TestInventory.java b/networking/src/test/java/ch/dissem/bitmessage/networking/TestInventory.java new file mode 100644 index 0000000..cefe316 --- /dev/null +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/TestInventory.java @@ -0,0 +1,81 @@ +/* + * 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.networking; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.payload.ObjectType; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.ports.Inventory; +import ch.dissem.bitmessage.utils.TestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TestInventory implements Inventory { + private final Map<InventoryVector, ObjectMessage> inventory; + + public TestInventory() { + this.inventory = new HashMap<>(); + } + + @Override + public List<InventoryVector> getInventory(long... streams) { + return new ArrayList<>(inventory.keySet()); + } + + @Override + public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { + return offer; + } + + @Override + public ObjectMessage getObject(InventoryVector vector) { + return inventory.get(vector); + } + + @Override + public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) { + return new ArrayList<>(inventory.values()); + } + + @Override + public void storeObject(ObjectMessage object) { + inventory.put(object.getInventoryVector(), object); + } + + @Override + public boolean contains(ObjectMessage object) { + return inventory.containsKey(object.getInventoryVector()); + } + + @Override + public void cleanup() { + + } + + public void init(String... resources) throws IOException { + inventory.clear(); + for (String resource : resources) { + int version = Integer.parseInt(resource.substring(1, 2)); + ObjectMessage obj = TestUtils.loadObjectMessage(version, resource); + inventory.put(obj.getInventoryVector(), obj); + } + } +} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java b/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java new file mode 100644 index 0000000..c3abd58 --- /dev/null +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java @@ -0,0 +1,44 @@ +/* + * 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.networking; + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.NodeRegistry; + +import java.util.Arrays; +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; + + public TestNodeRegistry(NetworkAddress... nodes) { + this.nodes = Arrays.asList(nodes); + } + + @Override + public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { + return nodes; + } + + @Override + public void offerAddresses(List<NetworkAddress> addresses) { + // Ignore + } +} diff --git a/networking/src/test/resources/V1Msg.payload b/networking/src/test/resources/V1Msg.payload new file mode 100644 index 0000000..ddf21cd Binary files /dev/null and b/networking/src/test/resources/V1Msg.payload differ diff --git a/networking/src/test/resources/V4Pubkey.payload b/networking/src/test/resources/V4Pubkey.payload new file mode 100644 index 0000000..a5e4c5c Binary files /dev/null and b/networking/src/test/resources/V4Pubkey.payload differ diff --git a/networking/src/test/resources/V5Broadcast.payload b/networking/src/test/resources/V5Broadcast.payload new file mode 100644 index 0000000..87c8c04 Binary files /dev/null and b/networking/src/test/resources/V5Broadcast.payload differ diff --git a/repositories/build.gradle b/repositories/build.gradle index 2f467b8..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.' } @@ -10,9 +10,13 @@ uploadArchives { } } +sourceCompatibility = 1.8 + dependencies { - compile project(':domain') - compile 'com.h2database:h2:1.4.187' + compile project(':core') compile 'org.flywaydb:flyway-core:3.2.1' - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' + testCompile 'com.h2database:h2:1.4.190' + testCompile 'org.mockito:mockito-core:1.10.19' + 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 f1e55d6..337d50a 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.sql.*; import java.util.Arrays; import java.util.LinkedList; @@ -96,9 +97,10 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed FROM Address WHERE " + where); while (rs.next()) { BitmessageAddress address; - Blob privateKeyBlob = rs.getBlob("private_key"); - if (privateKeyBlob != null) { - PrivateKey privateKey = PrivateKey.read(privateKeyBlob.getBinaryStream()); + + InputStream privateKeyStream = rs.getBinaryStream("private_key"); + if (privateKeyStream != null) { + PrivateKey privateKey = PrivateKey.read(privateKeyStream); address = new BitmessageAddress(privateKey); } else { address = new BitmessageAddress(rs.getString("address")); @@ -179,10 +181,9 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito if (data != null) { ByteArrayOutputStream out = new ByteArrayOutputStream(); data.writeUnencrypted(out); - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - ps.setBlob(parameterIndex, in); + ps.setBytes(parameterIndex, out.toByteArray()); } else { - ps.setBlob(parameterIndex, (Blob) null); + ps.setBytes(parameterIndex, null); } } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcConfig.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcConfig.java index 80e079b..7448b19 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcConfig.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcConfig.java @@ -28,9 +28,9 @@ import java.sql.SQLException; */ public class JdbcConfig { protected final Flyway flyway; - private final String dbUrl; - private final String dbUser; - private final String dbPassword; + protected final String dbUrl; + protected final String dbUser; + protected final String dbPassword; public JdbcConfig(String dbUrl, String dbUser, String dbPassword) { this.dbUrl = dbUrl; 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 8795388..d583a71 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java @@ -18,21 +18,20 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.payload.ObjectType; -import org.flywaydb.core.Flyway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.sql.*; +import java.sql.PreparedStatement; +import java.sql.SQLException; 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; @@ -81,10 +80,9 @@ abstract class JdbcHelper { if (data != null) { ByteArrayOutputStream os = new ByteArrayOutputStream(); data.write(os); - ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); - ps.setBlob(parameterIndex, is); + ps.setBytes(parameterIndex, os.toByteArray()); } else { - ps.setBlob(parameterIndex, (Blob) null); + 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 c7e8873..3336475 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java @@ -27,12 +27,17 @@ import org.slf4j.LoggerFactory; import java.sql.*; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.now; public class JdbcInventory extends JdbcHelper implements Inventory { private static final Logger LOG = LoggerFactory.getLogger(JdbcInventory.class); + private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>(); + public JdbcInventory(JdbcConfig config) { super(config); } @@ -40,35 +45,43 @@ public class JdbcInventory extends JdbcHelper implements Inventory { @Override public List<InventoryVector> getInventory(long... streams) { List<InventoryVector> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT hash FROM Inventory WHERE expires > " + now() + - " AND stream IN (" + join(streams) + ")"); - while (rs.next()) { - result.add(new InventoryVector(rs.getBytes("hash"))); - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); + for (long stream : streams) { + getCache(stream).entrySet().stream() + .filter(e -> e.getValue() > now()) + .forEach(e -> result.add(e.getKey())); } return result; } - private List<InventoryVector> getFullInventory(long... streams) { - List<InventoryVector> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT hash FROM Inventory WHERE stream IN (" + join(streams) + ")"); - while (rs.next()) { - result.add(new InventoryVector(rs.getBytes("hash"))); + + private Map<InventoryVector, Long> getCache(long stream) { + Map<InventoryVector, Long> result = cache.get(stream); + if (result == null) { + synchronized (cache) { + 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); + while (rs.next()) { + result.put(new InventoryVector(rs.getBytes("hash")), rs.getLong("expires")); + } + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); } return result; } @Override public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { - offer.removeAll(getFullInventory(streams)); + for (long stream : streams) { + offer.removeAll(getCache(stream).keySet()); + } return offer; } @@ -119,6 +132,9 @@ public class JdbcInventory extends JdbcHelper implements Inventory { @Override public void storeObject(ObjectMessage object) { + 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(); @@ -130,6 +146,7 @@ public class JdbcInventory extends JdbcHelper implements Inventory { ps.setLong(5, object.getType()); ps.setLong(6, object.getVersion()); ps.executeUpdate(); + getCache(object.getStream()).put(iv, object.getExpiresTime()); } catch (SQLException e) { LOG.debug("Error storing object of type " + object.getPayload().getClass().getSimpleName(), e); } catch (Exception e) { @@ -137,13 +154,21 @@ public class JdbcInventory extends JdbcHelper implements Inventory { } } + @Override + public boolean contains(ObjectMessage object) { + return getCache(object.getStream()).entrySet().stream() + .anyMatch(x -> x.getKey().equals(object.getInventoryVector())); + } + @Override public void cleanup() { try (Connection connection = config.getConnection()) { - // We delete only objects that expired 5 minutes ago or earlier, so we don't request objects we just deleted - connection.createStatement().executeUpdate("DELETE FROM Inventory WHERE expires < " + (now() - 300)); + connection.createStatement().executeUpdate("DELETE FROM Inventory WHERE expires < " + now(-5 * MINUTE)); } catch (SQLException e) { LOG.debug(e.getMessage(), e); } + for (Map<InventoryVector, Long> c : cache.values()) { + c.entrySet().removeIf(e -> e.getValue() < now(-5 * MINUTE)); + } } } 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 278163f..69890c3 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -22,10 +22,12 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.InputStream; import java.sql.*; import java.util.ArrayList; import java.util.Collection; @@ -84,6 +86,43 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito return result; } + @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 { + where = ""; + } + 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); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + return 0; + } + + @Override + public Plaintext getMessage(byte[] initialHash) { + List<Plaintext> plaintexts = find("initial_hash=X'" + Strings.hex(initialHash) + "'"); + switch (plaintexts.size()) { + case 0: + return null; + case 1: + return plaintexts.get(0); + default: + throw new RuntimeException("This shouldn't happen, found " + plaintexts.size() + + " messages, one or none was expected"); + } + } + @Override public List<Plaintext> findMessages(Label label) { return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"); @@ -99,6 +138,11 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito return find("status='" + status.name() + "'"); } + @Override + public List<Plaintext> findMessages(BitmessageAddress sender) { + return find("sender='" + sender.getAddress() + "'"); + } + private List<Plaintext> find(String where) { List<Plaintext> result = new LinkedList<>(); try (Connection connection = config.getConnection()) { @@ -106,14 +150,14 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito 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"); - Blob data = rs.getBlob("data"); + InputStream data = rs.getBinaryStream("data"); Plaintext.Type type = Plaintext.Type.valueOf(rs.getString("type")); - Plaintext.Builder builder = Plaintext.readWithoutSignature(type, data.getBinaryStream()); + Plaintext.Builder builder = Plaintext.readWithoutSignature(type, data); 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"))); @@ -144,12 +188,12 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito public void save(Plaintext message) { // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = ctx.getAddressRepo().getAddress(message.getFrom().getAddress()); + BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); } - ctx.getAddressRepo().save(message.getFrom()); + ctx.getAddressRepository().save(message.getFrom()); } } @@ -190,7 +234,8 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito private void insert(Connection connection, Plaintext message) throws SQLException, IOException { PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS); + "INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status, initial_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS); ps.setBytes(1, message.getInventoryVector() != null ? message.getInventoryVector().getHash() : null); ps.setString(2, message.getType().name()); ps.setString(3, message.getFrom().getAddress()); @@ -199,6 +244,7 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito ps.setLong(6, message.getSent()); ps.setLong(7, message.getReceived()); ps.setString(8, message.getStatus() != null ? message.getStatus().name() : null); + ps.setBytes(9, message.getInitialHash()); ps.executeUpdate(); @@ -210,20 +256,33 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito private void update(Connection connection, Plaintext message) throws SQLException, IOException { PreparedStatement ps = connection.prepareStatement( - "UPDATE Message SET iv=?, sent=?, received=?, status=? WHERE id=?"); + "UPDATE Message SET iv=?, sent=?, received=?, status=?, initial_hash=? 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.setBytes(5, message.getInitialHash()); + ps.setLong(6, (Long) message.getId()); ps.executeUpdate(); } @Override public void remove(Plaintext message) { try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - stmt.executeUpdate("DELETE FROM Message WHERE id = " + message.getId()); + try { + connection.setAutoCommit(false); + 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(); + } catch (SQLException e) { + try { + connection.rollback(); + } catch (SQLException e1) { + LOG.debug(e1.getMessage(), e); + } + LOG.error(e.getMessage(), e); + } } catch (SQLException e) { LOG.error(e.getMessage(), e); } 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..9268311 --- /dev/null +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java @@ -0,0 +1,93 @@ +package ch.dissem.bitmessage.repository; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.LinkedList; +import java.util.List; + +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository { + private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class); + + public JdbcProofOfWorkRepository(JdbcConfig config) { + super(config); + } + + @Override + public Item getItem(byte[] initialHash) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, extra_bytes FROM POW WHERE initial_hash=?"); + ps.setBytes(1, initialHash); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Blob data = rs.getBlob("data"); + return new Item( + Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), + rs.getLong("nonce_trials_per_byte"), + rs.getLong("extra_bytes") + ); + } else { + throw new RuntimeException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + @Override + public List<byte[]> getItems() { + try (Connection connection = config.getConnection()) { + List<byte[]> result = new LinkedList<>(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT initial_hash FROM POW"); + while (rs.next()) { + result.add(rs.getBytes("initial_hash")); + } + return result; + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + @Override + public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, nonce_trials_per_byte, extra_bytes) VALUES (?, ?, ?, ?, ?)"); + ps.setBytes(1, security().getInitialHash(object)); + writeBlob(ps, 2, object); + ps.setLong(3, object.getVersion()); + ps.setLong(4, nonceTrialsPerByte); + ps.setLong(5, extraBytes); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.debug("Error storing object of type " + object.getPayload().getClass().getSimpleName(), e); + throw new RuntimeException(e); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + @Override + public void removeObject(byte[] initialHash) { + try (Connection connection = config.getConnection()) { + PreparedStatement ps = connection.prepareStatement("DELETE FROM POW WHERE initial_hash=?"); + ps.setBytes(1, initialHash); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.debug(e.getMessage(), e); + } + } +} 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/nodes.txt b/repositories/src/main/resources/nodes.txt deleted file mode 100644 index b728ba5..0000000 --- a/repositories/src/main/resources/nodes.txt +++ /dev/null @@ -1,15 +0,0 @@ -[stream 1] - -[2604:2000:1380:9f:82e:148b:2746:d0c7]:8080 -5.45.99.75:8444 -75.167.159.54:8444 -95.165.168.168:8444 -85.180.139.241:8444 -158.222.211.81:8080 -178.62.12.187:8448 -24.188.198.204:8111 -109.147.204.113:1195 -178.11.46.221:8444 - -[stream 2] -# none yet \ No newline at end of file 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 80740ca..18fc83c 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java @@ -25,7 +25,7 @@ import java.util.List; import static org.junit.Assert.*; -public class JdbcAddressRepositoryTest { +public class JdbcAddressRepositoryTest extends TestBase { public static final String CONTACT_A = "BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt"; public static final String CONTACT_B = "BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj"; public static final String CONTACT_C = "BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke"; diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java index 8b23566..6f31299 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java @@ -34,7 +34,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; import static ch.dissem.bitmessage.utils.UnixTime.now; import static org.junit.Assert.*; -public class JdbcInventoryTest { +public class JdbcInventoryTest extends TestBase { private TestJdbcConfig config; private Inventory inventory; @@ -110,6 +110,17 @@ public class JdbcInventoryTest { assertNotNull(inventory.getObject(object.getInventoryVector())); } + @Test + public void testContains() { + ObjectMessage object = getObjectMessage(5, 0, getGetPubkey()); + + assertFalse(inventory.contains(object)); + + inventory.storeObject(object); + + assertTrue(inventory.contains(object)); + } + @Test public void testCleanup() throws Exception { assertNotNull(inventory.getObject(inventoryVectorIgnore)); 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 b99e44f..c7c3614 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -25,7 +25,6 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.utils.Security; import org.junit.Before; import org.junit.Test; @@ -33,10 +32,11 @@ import java.util.Arrays; 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; -public class JdbcMessageRepositoryTest { +public class JdbcMessageRepositoryTest extends TestBase { private BitmessageAddress contactA; private BitmessageAddress contactB; private BitmessageAddress identity; @@ -54,7 +54,11 @@ public class JdbcMessageRepositoryTest { config.reset(); addressRepo = new JdbcAddressRepository(config); repo = new JdbcMessageRepository(config); - new InternalContext(new BitmessageContext.Builder().addressRepo(addressRepo).messageRepo(repo)); + new InternalContext(new BitmessageContext.Builder() + .cryptography(security()) + .addressRepo(addressRepo) + .messageRepo(repo) + ); BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); contactA = new BitmessageAddress(tmp.getAddress()); @@ -120,7 +124,7 @@ public class JdbcMessageRepositoryTest { @Test public void testSave() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(new InventoryVector(Security.randomBytes(32))) + .IV(new InventoryVector(security().randomBytes(32))) .from(identity) .to(contactA) .message("Subject", "Message") @@ -143,7 +147,7 @@ public class JdbcMessageRepositoryTest { public void testUpdate() throws Exception { List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); Plaintext message = messages.get(0); - message.setInventoryVector(new InventoryVector(Security.randomBytes(32))); + message.setInventoryVector(new InventoryVector(security().randomBytes(32))); repo.save(message); messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); 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 d668822..044c44c 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.NodeRegistry; import org.junit.Before; import org.junit.Test; @@ -27,7 +28,7 @@ import java.util.List; import static ch.dissem.bitmessage.utils.UnixTime.now; import static org.junit.Assert.assertEquals; -public class JdbcNodeRegistryTest { +public class JdbcNodeRegistryTest extends TestBase { private TestJdbcConfig config; private NodeRegistry registry; diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java new file mode 100644 index 0000000..be386cd --- /dev/null +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.utils.Singleton; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by chris on 20.07.15. + */ +public class TestBase { + static { + BouncyCryptography security = new BouncyCryptography(); + Singleton.initialize(security); + InternalContext ctx = mock(InternalContext.class); + when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); + security.setContext(ctx); + } +} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java index 909ba75..99a26c0 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java @@ -17,7 +17,8 @@ package ch.dissem.bitmessage.repository; /** - * Created by chris on 02.06.15. + * JdbcConfig to be used for tests. Uses an in-memory database and adds a useful {@link #reset()} method resetting + * the database. */ public class TestJdbcConfig extends JdbcConfig { public TestJdbcConfig() { diff --git a/settings.gradle b/settings.gradle index 6452ec0..4caa813 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ rootProject.name = 'Jabit' -include 'domain' +include 'core' include 'networking' @@ -9,3 +9,9 @@ include 'repositories' include 'demo' include 'wif' + +include 'cryptography-sc' + +include 'cryptography-bc' + +include 'extensions' \ No newline at end of file diff --git a/wif/build.gradle b/wif/build.gradle index 63566a3..93a0248 100644 --- a/wif/build.gradle +++ b/wif/build.gradle @@ -11,8 +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.+' + testCompile 'org.mockito:mockito-core:1.10.19' + 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 26b6d63..b41d645 100644 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java +++ b/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java @@ -19,7 +19,6 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.utils.Base58; -import ch.dissem.bitmessage.utils.Security; import org.ini4j.Ini; import org.ini4j.Profile; @@ -27,6 +26,7 @@ import java.io.*; import java.util.Collection; import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * @author Christian Basler @@ -73,7 +73,7 @@ public class WifExporter { byte[] result = new byte[37]; result[0] = (byte) 0x80; System.arraycopy(privateKey, 0, result, 1, PRIVATE_KEY_SIZE); - byte[] hash = Security.doubleSha256(result, PRIVATE_KEY_SIZE + 1); + byte[] hash = security().doubleSha256(result, PRIVATE_KEY_SIZE + 1); System.arraycopy(hash, 0, result, PRIVATE_KEY_SIZE + 1, 4); return Base58.encode(result); } 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 45bbb4d..e88eaa4 100644 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java +++ b/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java @@ -21,7 +21,6 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Base58; -import ch.dissem.bitmessage.utils.Security; import org.ini4j.Ini; import org.ini4j.Profile; import org.slf4j.Logger; @@ -34,6 +33,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; +import static ch.dissem.bitmessage.utils.Singleton.security; + /** * @author Christian Basler */ @@ -84,7 +85,7 @@ public class WifImporter { if (bytes.length != 37) throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); - byte[] hash = Security.doubleSha256(bytes, 33); + byte[] hash = security().doubleSha256(bytes, 33); for (int i = 0; i < 4; i++) { if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat); } 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 8cb9264..b930c0a 100644 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java +++ b/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import org.junit.Before; import org.junit.Test; @@ -34,9 +35,11 @@ public class WifExporterTest { @Before public void setUp() throws Exception { ctx = new BitmessageContext.Builder() + .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(); @@ -70,14 +73,14 @@ public class WifExporterTest { @Test public void testAddIdentity() throws Exception { - String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]\n" + - "label = Nuked Address\n" + - "enabled = true\n" + - "decoy = false\n" + - "noncetrialsperbyte = 320\n" + - "payloadlengthextrabytes = 14000\n" + - "privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9\n" + - "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck\n\n"; + String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]" + System.lineSeparator() + + "label = Nuked Address" + System.lineSeparator() + + "enabled = true" + System.lineSeparator() + + "decoy = false" + System.lineSeparator() + + "noncetrialsperbyte = 320" + System.lineSeparator() + + "payloadlengthextrabytes = 14000" + System.lineSeparator() + + "privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9" + System.lineSeparator() + + "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck" + System.lineSeparator() + System.lineSeparator(); importer = new WifImporter(ctx, expected); exporter.addIdentity(importer.getIdentities().get(0)); assertEquals(expected, exporter.toString()); 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 2973efc..fe8e15c 100644 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java +++ b/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java @@ -19,6 +19,7 @@ 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.cryptography.bc.BouncyCryptography; import org.junit.Before; import org.junit.Test; @@ -37,9 +38,11 @@ public class WifImporterTest { @Before public void setUp() throws Exception { ctx = new BitmessageContext.Builder() + .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();