diff --git a/build.gradle b/build.gradle index 78bf578..2169e2f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,22 +1,38 @@ +buildscript { + ext.kotlin_version = '1.1.3' + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} plugins { - id 'com.github.ben-manes.versions' version '0.14.0' + id 'com.github.ben-manes.versions' version '0.15.0' + id "io.spring.dependency-management" version "1.0.3.RELEASE" } subprojects { - apply plugin: 'java' + apply plugin: 'kotlin' apply plugin: 'maven' apply plugin: 'signing' apply plugin: 'jacoco' apply plugin: 'gitflow-version' + apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.ben-manes.versions' sourceCompatibility = 1.7 + targetCompatibility = 1.7 group = 'ch.dissem.jabit' repositories { mavenCentral() maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7" + compile "org.jetbrains.kotlin:kotlin-reflect" + } test { testLogging { @@ -38,6 +54,14 @@ subprojects { archives javadocJar, sourcesJar } + jar { + manifest { + attributes 'Implementation-Title': "Jabit ${project.name.capitalize()}", + 'Implementation-Version': version + } + baseName "jabit-${project.name}" + } + signing { required { isRelease && project.getProperties().get("signing.keyId")?.length() > 0 } sign configurations.archives @@ -93,4 +117,31 @@ subprojects { } check.dependsOn jacocoTestReport + + dependencyManagement { + dependencies { + dependencySet(group: 'org.jetbrains.kotlin', version: "$kotlin_version") { + entry 'kotlin-stdlib-jre7' + entry 'kotlin-reflect' + } + dependencySet(group: 'org.slf4j', version: '1.7.25') { + entry 'slf4j-api' + entry 'slf4j-simple' + } + + dependency 'ch.dissem.msgpack:msgpack:1.0.0' + dependency 'org.bouncycastle:bcprov-jdk15on:1.57' + dependency 'com.madgag.spongycastle:prov:1.56.0.0' + dependency 'org.apache.commons:commons-lang3:3.6' + dependency 'org.flywaydb:flyway-core:4.2.0' + + dependency 'args4j:args4j:2.33' + dependency 'org.ini4j:ini4j:0.5.4' + dependency 'com.h2database:h2:1.4.196' + + dependency 'junit:junit:4.12' + dependency 'org.hamcrest:hamcrest-library:1.3' + dependency 'com.nhaarman:mockito-kotlin:1.5.0' + } + } } diff --git a/core/build.gradle b/core/build.gradle index 785507e..6af7a84 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -24,10 +24,28 @@ artifacts { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.25' + compile 'org.slf4j:slf4j-api' compile 'ch.dissem.msgpack:msgpack:1.0.0' testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' - testCompile 'org.mockito:mockito-core:2.7.21' + testCompile 'com.nhaarman:mockito-kotlin:1.5.0' testCompile project(':cryptography-bc') } + +def generatedResources = "${project.buildDir}/generated-resources/main" + +sourceSets { + main { + output.dir(generatedResources, builtBy: 'generateVersionInfo') + } +} +task('generateVersionInfo') { + doLast { + def dir = new File(generatedResources) + if (!dir.exists()) { + dir.mkdirs() + } + def file = new File(generatedResources, "version") + file.write(project.version.toString()) + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java deleted file mode 100644 index 29639e7..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ /dev/null @@ -1,437 +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.*; -import ch.dissem.bitmessage.entity.payload.Broadcast; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -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.List; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; -import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; -import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -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: - * - *

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 InternalContext ctx; - - private final Labeler labeler; - - private final boolean sendPubkeyOnIdentityCreation; - - private BitmessageContext(Builder builder) { - ctx = new InternalContext(builder); - labeler = builder.labeler; - ctx.getProofOfWorkService().doMissingProofOfWork(30_000); // TODO: this should be configurable - sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation; - if (builder.listener instanceof Listener.WithContext) { - ((Listener.WithContext) builder.listener).setContext(this); - } - } - - public AddressRepository addresses() { - return ctx.getAddressRepository(); - } - - public MessageRepository messages() { - return ctx.getMessageRepository(); - } - - public Labeler labeler() { - return labeler; - } - - public BitmessageAddress createIdentity(boolean shorter, Feature... features) { - final BitmessageAddress identity = new BitmessageAddress(new PrivateKey( - shorter, - ctx.getStreams()[0], - NETWORK_NONCE_TRIALS_PER_BYTE, - NETWORK_EXTRA_BYTES, - features - )); - ctx.getAddressRepository().save(identity); - if (sendPubkeyOnIdentityCreation) { - ctx.sendPubkey(identity, identity.getStream()); - } - return identity; - } - - public BitmessageAddress joinChan(String passphrase, String address) { - BitmessageAddress chan = BitmessageAddress.chan(address, passphrase); - chan.setAlias(passphrase); - ctx.getAddressRepository().save(chan); - return chan; - } - - public BitmessageAddress createChan(String passphrase) { - // FIXME: hardcoded stream number - BitmessageAddress chan = BitmessageAddress.chan(1, passphrase); - ctx.getAddressRepository().save(chan); - return chan; - } - - public List createDeterministicAddresses( - String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) { - List result = BitmessageAddress.deterministic( - passphrase, numberOfAddresses, version, stream, shorter); - for (int i = 0; i < result.size(); i++) { - BitmessageAddress address = result.get(i); - address.setAlias("deterministic (" + (i + 1) + ")"); - ctx.getAddressRepository().save(address); - } - return result; - } - - public void broadcast(final BitmessageAddress from, final String subject, final String message) { - Plaintext msg = new Plaintext.Builder(BROADCAST) - .from(from) - .message(subject, message) - .build(); - send(msg); - } - - public void send(final BitmessageAddress from, final BitmessageAddress to, final String subject, final String message) { - if (from.getPrivateKey() == null) { - throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); - } - Plaintext msg = new Plaintext.Builder(MSG) - .from(from) - .to(to) - .message(subject, message) - .build(); - send(msg); - } - - public void send(final Plaintext msg) { - if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) { - throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); - } - labeler().markAsSending(msg); - BitmessageAddress to = msg.getTo(); - if (to != null) { - if (to.getPubkey() == null) { - LOG.info("Public key is missing from recipient. Requesting."); - ctx.requestPubkey(to); - } - if (to.getPubkey() == null) { - ctx.getMessageRepository().save(msg); - } - } - if (to == null || to.getPubkey() != null) { - LOG.info("Sending message."); - ctx.getMessageRepository().save(msg); - if (msg.getType() == MSG) { - ctx.send(msg); - } else { - ctx.send( - msg.getFrom(), - to, - Factory.getBroadcast(msg), - msg.getTTL() - ); - } - } - } - - public void startup() { - ctx.getNetworkHandler().start(); - } - - 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, 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); - } - - /** - * Removes expired objects from the inventory. You should call this method regularly, - * e.g. daily and on each shutdown. - */ - public void cleanup() { - ctx.getInventory().cleanup(); - } - - /** - * Sends messages again whose time to live expired without being acknowledged. (And whose - * recipient is expected to send acknowledgements. - *

- * You should call this method regularly, but be aware of the following: - *

- */ - public void resendUnacknowledgedMessages() { - ctx.resendUnacknowledged(); - } - - public boolean isRunning() { - return ctx.getNetworkHandler().isRunning(); - } - - public void addContact(BitmessageAddress contact) { - ctx.getAddressRepository().save(contact); - if (contact.getPubkey() == null) { - BitmessageAddress stored = ctx.getAddressRepository().getAddress(contact.getAddress()); - if (stored.getPubkey() == null) { - ctx.requestPubkey(contact); - } - } - } - - 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); - // This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with - // other subscriptions and the interface stays as simple as possible. - ctx.getNetworkListener().receive(object); - } catch (DecryptionFailedException ignore) { - } catch (Exception e) { - LOG.debug(e.getMessage(), e); - } - } - } - - public Property status() { - return new Property("status", null, - ctx.getNetworkHandler().getNetworkStatus(), - new Property("unacknowledged", ctx.getMessageRepository().findMessagesToResend().size()) - ); - } - - /** - * 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); - - /** - * A message listener that needs a {@link BitmessageContext}, i.e. for implementing some sort of chat bot. - */ - interface WithContext extends Listener { - void setContext(BitmessageContext ctx); - } - } - - public static final class Builder { - int port = 8444; - Inventory inventory; - NodeRegistry nodeRegistry; - NetworkHandler networkHandler; - AddressRepository addressRepo; - MessageRepository messageRepo; - ProofOfWorkRepository proofOfWorkRepository; - ProofOfWorkEngine proofOfWorkEngine; - Cryptography cryptography; - CustomCommandHandler customCommandHandler; - Labeler labeler; - Listener listener; - int connectionLimit = 150; - long connectionTTL = 30 * MINUTE; - boolean sendPubkeyOnIdentityCreation = true; - - 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 customCommandHandler(CustomCommandHandler handler) { - this.customCommandHandler = handler; - return this; - } - - public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) { - this.proofOfWorkEngine = proofOfWorkEngine; - return this; - } - - public Builder labeler(Labeler labeler) { - this.labeler = labeler; - return this; - } - - public Builder listener(Listener listener) { - this.listener = listener; - return this; - } - - 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; - } - - 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 (labeler == null) { - labeler = new DefaultLabeler(); - } - if (customCommandHandler == null) { - customCommandHandler = new CustomCommandHandler() { - @Override - public MessagePayload handle(CustomMessage request) { - LOG.debug("Received custom request, but no custom command handler configured."); - return null; - } - }; - } - 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/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java deleted file mode 100644 index 4fb438f..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java +++ /dev/null @@ -1,191 +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.valueobject.InventoryVector; -import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.ports.Labeler; -import ch.dissem.bitmessage.ports.NetworkHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED; - -class DefaultMessageListener implements NetworkHandler.MessageListener, InternalContext.ContextHolder { - private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class); - private final Labeler labeler; - private final BitmessageContext.Listener listener; - private InternalContext ctx; - - public DefaultMessageListener(Labeler labeler, BitmessageContext.Listener listener) { - this.labeler = labeler; - this.listener = listener; - } - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } - - @Override - @SuppressWarnings("ConstantConditions") - public void receive(ObjectMessage object) throws IOException { - ObjectPayload payload = object.getPayload(); - if (payload.getType() == null) { - if (payload instanceof GenericPayload) { - receive((GenericPayload) payload); - } - return; - } - - switch (payload.getType()) { - case GET_PUBKEY: { - receive(object, (GetPubkey) payload); - break; - } - case PUBKEY: { - receive(object, (Pubkey) payload); - break; - } - case MSG: { - receive(object, (Msg) payload); - break; - } - case BROADCAST: { - receive(object, (Broadcast) payload); - break; - } - default: { - throw new IllegalArgumentException("Unknown payload type " + payload.getType()); - } - } - } - - protected void receive(ObjectMessage object, GetPubkey getPubkey) { - BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag()); - if (identity != null && identity.getPrivateKey() != null && !identity.isChan()) { - LOG.info("Got pubkey request for identity " + identity); - // FIXME: only send pubkey if it wasn't sent in the last TTL.pubkey() days - ctx.sendPubkey(identity, object.getStream()); - } - } - - protected void receive(ObjectMessage object, Pubkey pubkey) throws IOException { - BitmessageAddress address; - try { - if (pubkey instanceof V4Pubkey) { - V4Pubkey v4Pubkey = (V4Pubkey) pubkey; - address = ctx.getAddressRepository().findContact(v4Pubkey.getTag()); - if (address != null) { - v4Pubkey.decrypt(address.getPublicDecryptionKey()); - } - } else { - address = ctx.getAddressRepository().findContact(pubkey.getRipe()); - } - if (address != null && address.getPubkey() == null) { - 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 messages = ctx.getMessageRepository().findMessages(PUBKEY_REQUESTED, address); - LOG.info("Sending " + messages.size() + " messages for contact " + address); - for (Plaintext msg : messages) { - ctx.getLabeler().markAsSending(msg); - ctx.getMessageRepository().save(msg); - ctx.send(msg); - } - } - - protected void receive(ObjectMessage object, Msg msg) throws IOException { - for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) { - try { - msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey()); - Plaintext plaintext = msg.getPlaintext(); - plaintext.setTo(identity); - if (!object.isSignatureValid(plaintext.getFrom().getPubkey())) { - LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); - } else { - receive(object.getInventoryVector(), plaintext); - } - break; - } catch (DecryptionFailedException ignore) { - } - } - } - - protected void receive(GenericPayload ack) { - if (ack.getData().length == Msg.ACK_LENGTH) { - Plaintext msg = ctx.getMessageRepository().getMessageForAck(ack.getData()); - if (msg != null) { - ctx.getLabeler().markAsAcknowledged(msg); - ctx.getMessageRepository().save(msg); - } - } - } - - protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException { - byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null; - for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) { - if (tag != null && !Arrays.equals(tag, subscription.getTag())) { - continue; - } - try { - broadcast.decrypt(subscription.getPublicDecryptionKey()); - if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) { - LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); - } else { - receive(object.getInventoryVector(), broadcast.getPlaintext()); - } - } catch (DecryptionFailedException ignore) { - } - } - } - - protected void receive(InventoryVector iv, Plaintext msg) { - BitmessageAddress contact = ctx.getAddressRepository().getAddress(msg.getFrom().getAddress()); - if (contact != null && contact.getPubkey() == null) { - updatePubkey(contact, msg.getFrom().getPubkey()); - } - - msg.setInventoryVector(iv); - labeler.setLabels(msg); - ctx.getMessageRepository().save(msg); - listener.receive(msg); - - if (msg.getType() == Plaintext.Type.MSG && msg.getTo().has(Pubkey.Feature.DOES_ACK)) { - ObjectMessage ack = msg.getAckMessage(); - if (ack != null) { - ctx.getInventory().storeObject(ack); - ctx.getNetworkHandler().offer(ack.getInventoryVector()); - } - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java deleted file mode 100644 index 007e8f2..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java +++ /dev/null @@ -1,326 +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.*; -import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.TTL; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.TreeSet; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -/** - * 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 - * get extended. - * <p> - * On the other hand, if you need the BitmessageContext in a port implementation, the same thing might apply. - * </p> - */ -public class InternalContext { - private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class); - - public final static long NETWORK_NONCE_TRIALS_PER_BYTE = 1000; - public final static long NETWORK_EXTRA_BYTES = 1000; - - private final Executor threadPool = Executors.newCachedThreadPool(); - - 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 CustomCommandHandler customCommandHandler; - private final ProofOfWorkService proofOfWorkService; - private final Labeler labeler; - private final NetworkHandler.MessageListener networkListener; - - private final TreeSet<Long> streams = new TreeSet<>(); - private final int port; - private final long clientNonce; - 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 = cryptography.randomNonce(); - this.customCommandHandler = builder.customCommandHandler; - this.port = builder.port; - this.connectionLimit = builder.connectionLimit; - this.connectionTTL = builder.connectionTTL; - this.labeler = builder.labeler; - this.networkListener = new DefaultMessageListener(labeler, builder.listener); - - Singleton.initialize(cryptography); - - // TODO: streams of new identities and subscriptions should also be added. This works only after a restart. - for (BitmessageAddress address : addressRepository.getIdentities()) { - 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, customCommandHandler, builder.labeler, - networkListener); - for (BitmessageAddress identity : addressRepository.getIdentities()) { - streams.add(identity.getStream()); - } - } - - private void init(Object... objects) { - for (Object o : objects) { - if (o instanceof ContextHolder) { - ((ContextHolder) o).setContext(this); - } - } - } - - public Cryptography getCryptography() { - return cryptography; - } - - public Inventory getInventory() { - return inventory; - } - - public NodeRegistry getNodeRegistry() { - return nodeRegistry; - } - - public NetworkHandler getNetworkHandler() { - return networkHandler; - } - - public AddressRepository getAddressRepository() { - return addressRepository; - } - - public MessageRepository getMessageRepository() { - return messageRepository; - } - - public ProofOfWorkRepository getProofOfWorkRepository() { - return proofOfWorkRepository; - } - - public ProofOfWorkEngine getProofOfWorkEngine() { - return proofOfWorkEngine; - } - - public ProofOfWorkService getProofOfWorkService() { - return proofOfWorkService; - } - - public Labeler getLabeler() { - return labeler; - } - - public NetworkHandler.MessageListener getNetworkListener() { - return networkListener; - } - - public long[] getStreams() { - long[] result = new long[streams.size()]; - int i = 0; - for (long stream : streams) { - result[i++] = stream; - } - return result; - } - - public int getPort() { - return port; - } - - public void send(final Plaintext plaintext) { - if (plaintext.getAckMessage() != null) { - long expires = UnixTime.now(+plaintext.getTTL()); - LOG.info("Expires at " + expires); - proofOfWorkService.doProofOfWorkWithAck(plaintext, expires); - } else { - send(plaintext.getFrom(), plaintext.getTo(), new Msg(plaintext), plaintext.getTTL()); - } - } - - public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload, - final long timeToLive) { - try { - final BitmessageAddress recipient = (to != null ? to : from); - long expires = UnixTime.now(+timeToLive); - LOG.info("Expires at " + expires); - final ObjectMessage object = new ObjectMessage.Builder() - .stream(recipient.getStream()) - .expiresTime(expires) - .payload(payload) - .build(); - if (object.isSigned()) { - object.sign(from.getPrivateKey()); - } - if (payload instanceof Broadcast) { - ((Broadcast) payload).encrypt(); - } else if (payload instanceof Encrypted) { - object.encrypt(recipient.getPubkey()); - } - proofOfWorkService.doProofOfWork(to, object); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public void sendPubkey(final BitmessageAddress identity, final long targetStream) { - try { - long expires = UnixTime.now(TTL.pubkey()); - LOG.info("Expires at " + expires); - final ObjectMessage response = new ObjectMessage.Builder() - .stream(targetStream) - .expiresTime(expires) - .payload(identity.getPubkey()) - .build(); - response.sign(identity.getPrivateKey()); - response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey())); - // TODO: remember that the pubkey is just about to be sent, and on which stream! - proofOfWorkService.doProofOfWork(response); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - /** - * Be aware that if the pubkey already exists in the inventory, the metods will not request it and the callback - * for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB. - */ - public void requestPubkey(final BitmessageAddress contact) { - threadPool.execute(new Runnable() { - @Override - public void run() { - BitmessageAddress stored = addressRepository.getAddress(contact.getAddress()); - - tryToFindMatchingPubkey(contact); - if (contact.getPubkey() != null) { - if (stored != null) { - stored.setPubkey(contact.getPubkey()); - addressRepository.save(stored); - } else { - addressRepository.save(contact); - } - return; - } - - if (stored == null) { - addressRepository.save(contact); - } - - long expires = UnixTime.now(TTL.getpubkey()); - LOG.info("Expires at " + expires); - final ObjectMessage request = new ObjectMessage.Builder() - .stream(contact.getStream()) - .expiresTime(expires) - .payload(new GetPubkey(contact)) - .build(); - proofOfWorkService.doProofOfWork(request); - } - }); - } - - private void tryToFindMatchingPubkey(BitmessageAddress address) { - BitmessageAddress stored = addressRepository.getAddress(address.getAddress()); - if (stored != null) { - address.setAlias(stored.getAlias()); - address.setSubscribed(stored.isSubscribed()); - } - for (ObjectMessage object : inventory.getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) { - try { - Pubkey pubkey = (Pubkey) object.getPayload(); - if (address.getVersion() == 4) { - V4Pubkey v4Pubkey = (V4Pubkey) pubkey; - if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) { - v4Pubkey.decrypt(address.getPublicDecryptionKey()); - if (object.isSignatureValid(v4Pubkey)) { - address.setPubkey(v4Pubkey); - addressRepository.save(address); - break; - } else { - LOG.info("Found pubkey for " + address + " but signature is invalid"); - } - } - } else { - if (Arrays.equals(pubkey.getRipe(), address.getRipe())) { - address.setPubkey(pubkey); - addressRepository.save(address); - break; - } - } - } catch (Exception e) { - LOG.debug(e.getMessage(), e); - } - } - } - - public void resendUnacknowledged() { - List<Plaintext> messages = messageRepository.findMessagesToResend(); - for (Plaintext message : messages) { - send(message); - messageRepository.save(message); - } - } - - public long getClientNonce() { - return clientNonce; - } - - public long getConnectionTTL() { - return connectionTTL; - } - - public int getConnectionLimit() { - return connectionLimit; - } - - public CustomCommandHandler getCustomCommandHandler() { - return customCommandHandler; - } - - public interface ContextHolder { - void setContext(InternalContext context); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java deleted file mode 100644 index 14e7c8a..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java +++ /dev/null @@ -1,125 +0,0 @@ -package ch.dissem.bitmessage; - -import ch.dissem.bitmessage.entity.*; -import ch.dissem.bitmessage.entity.payload.Msg; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.ports.Cryptography; -import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.ports.ProofOfWorkRepository; -import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; -import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * @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(long delayInMilliseconds) { - final List<byte[]> items = powRepo.getItems(); - if (items.isEmpty()) return; - - // Wait for 30 seconds, to let the application start up before putting heavy load on the CPU - new Timer().schedule(new TimerTask() { - @Override - public void run() { - LOG.info("Doing POW for " + items.size() + " tasks."); - for (byte[] initialHash : items) { - Item item = powRepo.getItem(initialHash); - cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, - ProofOfWorkService.this); - } - } - }, delayInMilliseconds); - } - - public void doProofOfWork(ObjectMessage object) { - doProofOfWork(null, object); - } - - public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) { - Pubkey pubkey = recipient == null ? null : recipient.getPubkey(); - - long nonceTrialsPerByte = pubkey == null ? NETWORK_NONCE_TRIALS_PER_BYTE : pubkey.getNonceTrialsPerByte(); - long extraBytes = pubkey == null ? NETWORK_EXTRA_BYTES : pubkey.getExtraBytes(); - - powRepo.putObject(object, nonceTrialsPerByte, extraBytes); - if (object.getPayload() instanceof PlaintextHolder) { - Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext(); - plaintext.setInitialHash(cryptography.getInitialHash(object)); - messageRepo.save(plaintext); - } - cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this); - } - - public void doProofOfWorkWithAck(Plaintext plaintext, long expirationTime) { - final ObjectMessage ack = plaintext.getAckMessage(); - messageRepo.save(plaintext); - Item item = new Item(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, - expirationTime, plaintext); - powRepo.putObject(item); - cryptography.doProofOfWork(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, this); - } - - @Override - public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - Item item = powRepo.getItem(initialHash); - if (item.message == null) { - ObjectMessage object = item.object; - object.setNonce(nonce); - Plaintext plaintext = messageRepo.getMessage(initialHash); - if (plaintext != null) { - plaintext.setInventoryVector(object.getInventoryVector()); - plaintext.updateNextTry(); - ctx.getLabeler().markAsSent(plaintext); - messageRepo.save(plaintext); - } - try { - ctx.getNetworkListener().receive(object); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - ctx.getInventory().storeObject(object); - ctx.getNetworkHandler().offer(object.getInventoryVector()); - } else { - item.message.getAckMessage().setNonce(nonce); - final ObjectMessage object = new ObjectMessage.Builder() - .stream(item.message.getStream()) - .expiresTime(item.expirationTime) - .payload(new Msg(item.message)) - .build(); - if (object.isSigned()) { - object.sign(item.message.getFrom().getPrivateKey()); - } - if (object.getPayload() instanceof Encrypted) { - object.encrypt(item.message.getTo().getPubkey()); - } - doProofOfWork(item.message.getTo(), object); - } - powRepo.removeObject(initialHash); - } - - @Override - public void setContext(InternalContext ctx) { - this.ctx = ctx; - this.cryptography = cryptography(); - this.powRepo = ctx.getProofOfWorkRepository(); - this.messageRepo = ctx.getMessageRepository(); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java b/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java deleted file mode 100644 index 73d9995..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java +++ /dev/null @@ -1,83 +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.entity; - -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * The 'addr' command holds a list of known active Bitmessage nodes. - */ -public class Addr implements MessagePayload { - private static final long serialVersionUID = -5117688017050138720L; - - private final List<NetworkAddress> addresses; - - private Addr(Builder builder) { - addresses = builder.addresses; - } - - @Override - public Command getCommand() { - return Command.ADDR; - } - - public List<NetworkAddress> getAddresses() { - return addresses; - } - - @Override - public void write(OutputStream out) throws IOException { - Encode.varInt(addresses.size(), out); - for (NetworkAddress address : addresses) { - address.write(out); - } - } - - @Override - public void write(ByteBuffer buffer) { - Encode.varInt(addresses.size(), buffer); - for (NetworkAddress address : addresses) { - address.write(buffer); - } - } - - public static final class Builder { - private List<NetworkAddress> addresses = new ArrayList<>(); - - public Builder addresses(Collection<NetworkAddress> addresses){ - this.addresses.addAll(addresses); - return this; - } - - public Builder addAddress(final NetworkAddress address) { - this.addresses.add(address); - return this; - } - - public Addr build() { - return new Addr(this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java b/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java deleted file mode 100644 index ce199c4..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java +++ /dev/null @@ -1,277 +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.entity; - -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.payload.V4Pubkey; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.utils.AccessCounter; -import ch.dissem.bitmessage.utils.Base58; -import ch.dissem.bitmessage.utils.Bytes; -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.ArrayList; -import java.util.Arrays; -import java.util.List; -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.cryptography; - -/** - * 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 implements Serializable { - private static final long serialVersionUID = 2386328540805994064L; - - private final long version; - private final long stream; - private final byte[] ripe; - private final byte[] tag; - /** - * Used for V4 address encryption. It's easier to just create it regardless of address version. - */ - private final byte[] publicDecryptionKey; - - private String address; - - private PrivateKey privateKey; - private Pubkey pubkey; - - private String alias; - private boolean subscribed; - private boolean chan; - - BitmessageAddress(long version, long stream, byte[] ripe) { - try { - this.version = version; - this.stream = stream; - this.ripe = ripe; - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - Encode.varInt(version, os); - Encode.varInt(stream, os); - if (version < 4) { - byte[] checksum = cryptography().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 = cryptography().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 = cryptography().doubleSha512(os.toByteArray()); - os.write(checksum, 0, 4); - this.address = "BM-" + Base58.encode(os.toByteArray()); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public BitmessageAddress(Pubkey publicKey) { - this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe()); - this.pubkey = publicKey; - } - - public BitmessageAddress(String address, String passphrase) { - this(address); - this.privateKey = new PrivateKey(this, passphrase); - this.pubkey = this.privateKey.getPubkey(); - if (!Arrays.equals(ripe, privateKey.getPubkey().getRipe())) { - throw new IllegalArgumentException("Wrong address or passphrase"); - } - } - - public static BitmessageAddress chan(String address, String passphrase) { - BitmessageAddress result = new BitmessageAddress(address, passphrase); - result.chan = true; - return result; - } - - public static BitmessageAddress chan(long stream, String passphrase) { - PrivateKey privateKey = new PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase); - BitmessageAddress result = new BitmessageAddress(privateKey); - result.chan = true; - return result; - } - - public static List<BitmessageAddress> deterministic(String passphrase, int numberOfAddresses, - long version, long stream, boolean shorter) { - List<BitmessageAddress> result = new ArrayList<>(numberOfAddresses); - List<PrivateKey> privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter); - for (PrivateKey pk : privateKeys) { - result.add(new BitmessageAddress(pk)); - } - return result; - } - - public BitmessageAddress(PrivateKey privateKey) { - this(privateKey.getPubkey()); - this.privateKey = privateKey; - } - - public BitmessageAddress(String address) { - try { - this.address = address; - byte[] bytes = Base58.decode(address.substring(3)); - ByteArrayInputStream in = new ByteArrayInputStream(bytes); - AccessCounter counter = new AccessCounter(); - this.version = varInt(in, counter); - this.stream = varInt(in, counter); - this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20); - - // test checksum - byte[] checksum = cryptography().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 = cryptography().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); - this.tag = null; - this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); - } else { - checksum = cryptography().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); - this.tag = Arrays.copyOfRange(checksum, 32, 64); - this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); - } - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public static byte[] calculateTag(long version, long stream, byte[] ripe) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Encode.varInt(version, out); - Encode.varInt(stream, out); - out.write(ripe); - return Arrays.copyOfRange(cryptography().doubleSha512(out.toByteArray()), 32, 64); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public long getStream() { - return stream; - } - - public long getVersion() { - return version; - } - - public Pubkey getPubkey() { - return pubkey; - } - - public void setPubkey(Pubkey pubkey) { - if (pubkey instanceof V4Pubkey) { - if (!Arrays.equals(tag, ((V4Pubkey) pubkey).getTag())) - throw new IllegalArgumentException("Pubkey has incompatible tag"); - } - if (!Arrays.equals(ripe, pubkey.getRipe())) - throw new IllegalArgumentException("Pubkey has incompatible ripe"); - this.pubkey = pubkey; - } - - /** - * @return the private key used to decrypt Pubkey objects (for v4 addresses) and broadcasts. - */ - public byte[] getPublicDecryptionKey() { - return publicDecryptionKey; - } - - public PrivateKey getPrivateKey() { - return privateKey; - } - - public String getAddress() { - return address; - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - @Override - public String toString() { - return alias == null ? address : alias; - } - - public byte[] getRipe() { - return ripe; - } - - public byte[] getTag() { - return tag; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BitmessageAddress address = (BitmessageAddress) o; - return Objects.equals(version, address.version) && - Objects.equals(stream, address.stream) && - Arrays.equals(ripe, address.ripe); - } - - @Override - public int hashCode() { - return Arrays.hashCode(ripe); - } - - public boolean isSubscribed() { - return subscribed; - } - - public void setSubscribed(boolean subscribed) { - this.subscribed = subscribed; - } - - public boolean isChan() { - return chan; - } - - public void setChan(boolean chan) { - this.chan = chan; - } - - public boolean has(Feature feature) { - if (pubkey == null || feature == null) { - return false; - } - return feature.isActive(pubkey.getBehaviorBitfield()); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java deleted file mode 100644 index 439f003..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java +++ /dev/null @@ -1,111 +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.entity; - -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.utils.AccessCounter; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.*; -import java.nio.ByteBuffer; - -import static ch.dissem.bitmessage.utils.Decode.bytes; -import static ch.dissem.bitmessage.utils.Decode.varString; - -/** - * @author Christian Basler - */ -public class CustomMessage implements MessagePayload { - private static final long serialVersionUID = -8932056829480326011L; - - public static final String COMMAND_ERROR = "ERROR"; - - private final String command; - private final byte[] data; - - public CustomMessage(String command) { - this.command = command; - this.data = null; - } - - public CustomMessage(String command, byte[] data) { - this.command = command; - this.data = data; - } - - public static CustomMessage read(InputStream in, int length) throws IOException { - AccessCounter counter = new AccessCounter(); - return new CustomMessage(varString(in, counter), bytes(in, length - counter.length())); - } - - @Override - public Command getCommand() { - return Command.CUSTOM; - } - - public String getCustomCommand() { - return command; - } - - public byte[] getData() { - if (data != null) { - return data; - } else { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - write(out); - return out.toByteArray(); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - } - - @Override - public void write(OutputStream out) throws IOException { - if (data != null) { - Encode.varString(command, out); - out.write(data); - } else { - throw new ApplicationException("Tried to write custom message without data. " + - "Programmer: did you forget to override #write()?"); - } - } - - @Override - public void write(ByteBuffer buffer) { - if (data != null) { - Encode.varString(command, buffer); - buffer.put(data); - } else { - throw new ApplicationException("Tried to write custom message without data. " + - "Programmer: did you forget to override #write()?"); - } - } - - public boolean isError() { - return COMMAND_ERROR.equals(command); - } - - public static CustomMessage error(String message) { - try { - return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java b/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java deleted file mode 100644 index 7d14fa0..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java +++ /dev/null @@ -1,84 +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.entity; - -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -/** - * The 'getdata' command is used to request objects from a node. - */ -public class GetData implements MessagePayload { - private static final long serialVersionUID = 1433878785969631061L; - - public static final int MAX_INVENTORY_SIZE = 50_000; - - List<InventoryVector> inventory; - - private GetData(Builder builder) { - inventory = builder.inventory; - } - - @Override - public Command getCommand() { - return Command.GETDATA; - } - - public List<InventoryVector> getInventory() { - return inventory; - } - - @Override - public void write(OutputStream out) throws IOException { - Encode.varInt(inventory.size(), out); - for (InventoryVector iv : inventory) { - iv.write(out); - } - } - - @Override - public void write(ByteBuffer buffer) { - Encode.varInt(inventory.size(), buffer); - for (InventoryVector iv : inventory) { - iv.write(buffer); - } - } - - public static final class Builder { - private List<InventoryVector> inventory = new LinkedList<>(); - - public Builder addInventoryVector(InventoryVector inventoryVector) { - this.inventory.add(inventoryVector); - return this; - } - - public Builder inventory(List<InventoryVector> inventory) { - this.inventory = inventory; - return this; - } - - public GetData build() { - return new GetData(this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java b/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java deleted file mode 100644 index 8d0f592..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java +++ /dev/null @@ -1,82 +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.entity; - -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -/** - * The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items. - */ -public class Inv implements MessagePayload { - private static final long serialVersionUID = 3662992522956947145L; - - private List<InventoryVector> inventory; - - private Inv(Builder builder) { - inventory = builder.inventory; - } - - public List<InventoryVector> getInventory() { - return inventory; - } - - @Override - public Command getCommand() { - return Command.INV; - } - - @Override - public void write(OutputStream out) throws IOException { - Encode.varInt(inventory.size(), out); - for (InventoryVector iv : inventory) { - iv.write(out); - } - } - - @Override - public void write(ByteBuffer buffer) { - Encode.varInt(inventory.size(), buffer); - for (InventoryVector iv : inventory) { - iv.write(buffer); - } - } - - public static final class Builder { - private List<InventoryVector> inventory = new LinkedList<>(); - - public Builder addInventoryVector(InventoryVector inventoryVector) { - this.inventory.add(inventoryVector); - return this; - } - - public Builder inventory(List<InventoryVector> inventory) { - this.inventory = inventory; - return this; - } - - public Inv build() { - return new Inv(this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java deleted file mode 100644 index f27384e..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java +++ /dev/null @@ -1,151 +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.entity; - -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * A network message is exchanged between two nodes. - */ -public class NetworkMessage implements Streamable { - private static final long serialVersionUID = 702708857104464809L; - - /** - * Magic value indicating message origin network, and used to seek to next message when stream state is unknown - */ - public final static int MAGIC = 0xE9BEB4D9; - public final static byte[] MAGIC_BYTES = ByteBuffer.allocate(4).putInt(MAGIC).array(); - - private final MessagePayload payload; - - public NetworkMessage(MessagePayload payload) { - this.payload = payload; - } - - /** - * First 4 bytes of sha512(payload) - */ - private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException { - byte[] d = cryptography().sha512(bytes); - return new byte[]{d[0], d[1], d[2], d[3]}; - } - - /** - * The actual data, a message or an object. Not to be confused with objectPayload. - */ - public MessagePayload getPayload() { - return payload; - } - - @Override - public void write(OutputStream out) throws IOException { - // magic - Encode.int32(MAGIC, out); - - // ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected) - String command = payload.getCommand().name().toLowerCase(); - out.write(command.getBytes("ASCII")); - for (int i = command.length(); i < 12; i++) { - out.write('\0'); - } - - byte[] payloadBytes = Encode.bytes(payload); - - // Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would - // ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are - // larger than this. - Encode.int32(payloadBytes.length, out); - - // checksum - try { - out.write(getChecksum(payloadBytes)); - } catch (GeneralSecurityException e) { - throw new ApplicationException(e); - } - - // message payload - out.write(payloadBytes); - } - - /** - * A more efficient implementation of the write method, writing header data to the provided buffer and returning - * a new buffer containing the payload. - * - * @param headerBuffer where the header data is written to (24 bytes) - * @return a buffer containing the payload, ready to be read. - */ - public ByteBuffer writeHeaderAndGetPayloadBuffer(ByteBuffer headerBuffer) { - return ByteBuffer.wrap(writeHeader(headerBuffer)); - } - - /** - * For improved memory efficiency, you should use {@link #writeHeaderAndGetPayloadBuffer(ByteBuffer)} - * and write the header buffer as well as the returned payload buffer into the channel. - * - * @param buffer where everything gets written to. Needs to be large enough for the whole message - * to be written. - */ - @Override - public void write(ByteBuffer buffer) { - byte[] payloadBytes = writeHeader(buffer); - buffer.put(payloadBytes); - } - - private byte[] writeHeader(ByteBuffer out) { - // magic - Encode.int32(MAGIC, out); - - // ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected) - String command = payload.getCommand().name().toLowerCase(); - try { - out.put(command.getBytes("ASCII")); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - for (int i = command.length(); i < 12; i++) { - out.put((byte) 0); - } - - byte[] payloadBytes = Encode.bytes(payload); - - // Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would - // ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are - // larger than this. - Encode.int32(payloadBytes.length, out); - - // checksum - try { - out.put(getChecksum(payloadBytes)); - } catch (GeneralSecurityException e) { - throw new ApplicationException(e); - } - - // message payload - return payloadBytes; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java deleted file mode 100644 index 8e386f7..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java +++ /dev/null @@ -1,271 +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.entity; - -import ch.dissem.bitmessage.entity.payload.ObjectPayload; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.utils.Bytes; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Objects; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * The 'object' command sends an object that is shared throughout the network. - */ -public class ObjectMessage implements MessagePayload { - private static final long serialVersionUID = 2495752480120659139L; - - private byte[] nonce; - private long expiresTime; - private long objectType; - /** - * The object's version - */ - private long version; - private long stream; - - private ObjectPayload payload; - private byte[] payloadBytes; - - private ObjectMessage(Builder builder) { - nonce = builder.nonce; - expiresTime = builder.expiresTime; - objectType = builder.objectType; - version = builder.payload.getVersion(); - stream = builder.streamNumber > 0 ? builder.streamNumber : builder.payload.getStream(); - payload = builder.payload; - } - - @Override - public Command getCommand() { - return Command.OBJECT; - } - - public byte[] getNonce() { - return nonce; - } - - public void setNonce(byte[] nonce) { - this.nonce = nonce; - } - - public long getExpiresTime() { - return expiresTime; - } - - public long getType() { - return objectType; - } - - public ObjectPayload getPayload() { - return payload; - } - - public long getVersion() { - return version; - } - - public long getStream() { - return stream; - } - - public InventoryVector getInventoryVector() { - return InventoryVector.fromHash( - Bytes.truncate(cryptography().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32) - ); - } - - private boolean isEncrypted() { - return payload instanceof Encrypted && !((Encrypted) payload).isDecrypted(); - } - - public boolean isSigned() { - return payload.isSigned(); - } - - private byte[] getBytesToSign() { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - writeHeaderWithoutNonce(out); - payload.writeBytesToSign(out); - return out.toByteArray(); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public void sign(PrivateKey key) { - if (payload.isSigned()) { - payload.setSignature(cryptography().getSignature(getBytesToSign(), key)); - } - } - - public void decrypt(PrivateKey key) throws IOException, DecryptionFailedException { - if (payload instanceof Encrypted) { - ((Encrypted) payload).decrypt(key.getPrivateEncryptionKey()); - } - } - - public void decrypt(byte[] privateEncryptionKey) throws IOException, DecryptionFailedException { - if (payload instanceof Encrypted) { - ((Encrypted) payload).decrypt(privateEncryptionKey); - } - } - - public void encrypt(byte[] publicEncryptionKey) throws IOException { - if (payload instanceof Encrypted) { - ((Encrypted) payload).encrypt(publicEncryptionKey); - } - } - - public void encrypt(Pubkey publicKey) { - try { - if (payload instanceof Encrypted) { - ((Encrypted) payload).encrypt(publicKey.getEncryptionKey()); - } - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public boolean isSignatureValid(Pubkey pubkey) throws IOException { - if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first"); - return cryptography().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); - } - - @Override - public void write(OutputStream out) throws IOException { - if (nonce == null) { - out.write(new byte[8]); - } else { - out.write(nonce); - } - out.write(getPayloadBytesWithoutNonce()); - } - - @Override - public void write(ByteBuffer buffer) { - if (nonce == null) { - buffer.put(new byte[8]); - } else { - buffer.put(nonce); - } - buffer.put(getPayloadBytesWithoutNonce()); - } - - private void writeHeaderWithoutNonce(OutputStream out) throws IOException { - Encode.int64(expiresTime, out); - Encode.int32(objectType, out); - Encode.varInt(version, out); - Encode.varInt(stream, out); - } - - public byte[] getPayloadBytesWithoutNonce() { - try { - if (payloadBytes == null) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - writeHeaderWithoutNonce(out); - payload.write(out); - payloadBytes = out.toByteArray(); - } - return payloadBytes; - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - public static final class Builder { - private byte[] nonce; - private long expiresTime; - private long objectType = -1; - private long streamNumber; - private ObjectPayload payload; - - public Builder nonce(byte[] nonce) { - this.nonce = nonce; - return this; - } - - public Builder expiresTime(long expiresTime) { - this.expiresTime = expiresTime; - return this; - } - - public Builder objectType(long objectType) { - this.objectType = objectType; - return this; - } - - public Builder objectType(ObjectType objectType) { - this.objectType = objectType.getNumber(); - return this; - } - - public Builder stream(long streamNumber) { - this.streamNumber = streamNumber; - return this; - } - - public Builder payload(ObjectPayload payload) { - this.payload = payload; - if (this.objectType == -1) - this.objectType = payload.getType().getNumber(); - return this; - } - - public ObjectMessage build() { - return new ObjectMessage(this); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ObjectMessage that = (ObjectMessage) o; - - return expiresTime == that.expiresTime && - objectType == that.objectType && - version == that.version && - stream == that.stream && - Objects.equals(payload, that.payload); - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(nonce); - result = 31 * result + (int) (expiresTime ^ (expiresTime >>> 32)); - result = 31 * result + (int) (objectType ^ (objectType >>> 32)); - result = 31 * result + (int) (version ^ (version >>> 32)); - result = 31 * result + (int) (stream ^ (stream >>> 32)); - result = 31 * result + (payload != null ? payload.hashCode() : 0); - return result; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java deleted file mode 100644 index 141edfc..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ /dev/null @@ -1,733 +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.entity; - -import ch.dissem.bitmessage.entity.payload.Msg; -import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; -import ch.dissem.bitmessage.entity.valueobject.extended.Message; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.utils.*; - -import java.io.*; -import java.nio.ByteBuffer; -import java.util.*; -import java.util.Collections; - -import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; -import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * The unencrypted message to be sent by 'msg' or 'broadcast'. - */ -public class Plaintext implements Streamable { - private static final long serialVersionUID = -5325729856394951079L; - - private final Type type; - private final BitmessageAddress from; - private final long encoding; - private final byte[] message; - private final byte[] ackData; - private final UUID conversationId; - private ExtendedEncoding extendedData; - private ObjectMessage ackMessage; - private Object id; - private InventoryVector inventoryVector; - private BitmessageAddress to; - private byte[] signature; - private Status status; - private Long sent; - private Long received; - - private Set<Label> labels; - private byte[] initialHash; - - private long ttl; - private int retries; - private Long nextTry; - - private Plaintext(Builder builder) { - id = builder.id; - inventoryVector = builder.inventoryVector; - type = builder.type; - from = builder.from; - to = builder.to; - encoding = builder.encoding; - message = builder.message; - ackData = builder.ackData; - if (builder.ackMessage != null && builder.ackMessage.length > 0) { - ackMessage = Factory.getObjectMessage( - 3, - new ByteArrayInputStream(builder.ackMessage), - builder.ackMessage.length); - } - signature = builder.signature; - status = builder.status; - sent = builder.sent; - received = builder.received; - labels = builder.labels; - ttl = builder.ttl; - retries = builder.retries; - nextTry = builder.nextTry; - conversationId = builder.conversation; - } - - public static Plaintext read(Type type, InputStream in) throws IOException { - return readWithoutSignature(type, in) - .signature(Decode.varBytes(in)) - .received(UnixTime.now()) - .build(); - } - - public static Plaintext.Builder readWithoutSignature(Type type, InputStream in) throws IOException { - long version = Decode.varInt(in); - return new Builder(type) - .addressVersion(version) - .stream(Decode.varInt(in)) - .behaviorBitfield(Decode.int32(in)) - .publicSigningKey(Decode.bytes(in, 64)) - .publicEncryptionKey(Decode.bytes(in, 64)) - .nonceTrialsPerByte(version >= 3 ? Decode.varInt(in) : 0) - .extraBytes(version >= 3 ? Decode.varInt(in) : 0) - .destinationRipe(type == Type.MSG ? Decode.bytes(in, 20) : null) - .encoding(Decode.varInt(in)) - .message(Decode.varBytes(in)) - .ackMessage(type == Type.MSG ? Decode.varBytes(in) : null); - } - - public InventoryVector getInventoryVector() { - return inventoryVector; - } - - public void setInventoryVector(InventoryVector inventoryVector) { - this.inventoryVector = inventoryVector; - } - - public Type getType() { - return type; - } - - public byte[] getMessage() { - return message; - } - - public BitmessageAddress getFrom() { - return from; - } - - public BitmessageAddress getTo() { - return to; - } - - public void setTo(BitmessageAddress to) { - if (this.to.getVersion() != 0) - throw new IllegalStateException("Correct address already set"); - if (!Arrays.equals(this.to.getRipe(), to.getRipe())) { - throw new IllegalArgumentException("RIPEs don't match"); - } - this.to = to; - } - - public Set<Label> getLabels() { - return labels; - } - - public Encoding getEncoding() { - return Encoding.fromCode(encoding); - } - - public long getStream() { - return from.getStream(); - } - - public byte[] getSignature() { - return signature; - } - - public void setSignature(byte[] signature) { - 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); - if (from.getPubkey() == null) { - Encode.int32(0, out); - byte[] empty = new byte[64]; - out.write(empty); - out.write(empty); - if (from.getVersion() >= 3) { - Encode.varInt(0, out); - Encode.varInt(0, out); - } - } else { - Encode.int32(from.getPubkey().getBehaviorBitfield(), out); - out.write(from.getPubkey().getSigningKey(), 1, 64); - out.write(from.getPubkey().getEncryptionKey(), 1, 64); - if (from.getVersion() >= 3) { - Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), out); - Encode.varInt(from.getPubkey().getExtraBytes(), out); - } - } - if (type == Type.MSG) { - out.write(to.getRipe()); - } - Encode.varInt(encoding, out); - Encode.varInt(message.length, out); - out.write(message); - if (type == Type.MSG) { - if (to.has(Feature.DOES_ACK) && getAckMessage() != null) { - ByteArrayOutputStream ack = new ByteArrayOutputStream(); - getAckMessage().write(ack); - Encode.varBytes(ack.toByteArray(), out); - } else { - Encode.varInt(0, out); - } - } - if (includeSignature) { - if (signature == null) { - Encode.varInt(0, out); - } else { - Encode.varInt(signature.length, out); - out.write(signature); - } - } - } - - public void write(ByteBuffer buffer, boolean includeSignature) { - Encode.varInt(from.getVersion(), buffer); - Encode.varInt(from.getStream(), buffer); - if (from.getPubkey() == null) { - Encode.int32(0, buffer); - byte[] empty = new byte[64]; - buffer.put(empty); - buffer.put(empty); - if (from.getVersion() >= 3) { - Encode.varInt(0, buffer); - Encode.varInt(0, buffer); - } - } else { - Encode.int32(from.getPubkey().getBehaviorBitfield(), buffer); - buffer.put(from.getPubkey().getSigningKey(), 1, 64); - buffer.put(from.getPubkey().getEncryptionKey(), 1, 64); - if (from.getVersion() >= 3) { - Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), buffer); - Encode.varInt(from.getPubkey().getExtraBytes(), buffer); - } - } - if (type == Type.MSG) { - buffer.put(to.getRipe()); - } - Encode.varInt(encoding, buffer); - Encode.varInt(message.length, buffer); - buffer.put(message); - if (type == Type.MSG) { - if (to.has(Feature.DOES_ACK) && getAckMessage() != null) { - Encode.varBytes(Encode.bytes(getAckMessage()), buffer); - } else { - Encode.varInt(0, buffer); - } - } - if (includeSignature) { - if (signature == null) { - Encode.varInt(0, buffer); - } else { - Encode.varInt(signature.length, buffer); - buffer.put(signature); - } - } - } - - @Override - public void write(OutputStream out) throws IOException { - write(out, true); - } - - @Override - public void write(ByteBuffer buffer) { - write(buffer, true); - } - - public Object getId() { - return id; - } - - public void setId(long id) { - if (this.id != null) throw new IllegalStateException("ID already set"); - this.id = id; - } - - public Long getSent() { - return sent; - } - - public Long getReceived() { - return received; - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - if (status != Status.RECEIVED && sent == null && status != Status.DRAFT) { - sent = UnixTime.now(); - } - this.status = status; - } - - public long getTTL() { - return ttl; - } - - public int getRetries() { - return retries; - } - - public Long getNextTry() { - return nextTry; - } - - public void updateNextTry() { - if (to != null) { - if (nextTry == null) { - if (sent != null && to.has(Feature.DOES_ACK)) { - nextTry = UnixTime.now(+ttl); - retries++; - } - } else { - nextTry = nextTry + (1 << retries) * ttl; - retries++; - } - } - } - - public String getSubject() { - Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); - String firstLine = s.nextLine(); - if (encoding == EXTENDED.code) { - if (Message.TYPE.equals(getExtendedData().getType())) { - return ((Message) extendedData.getContent()).getSubject(); - } else { - return null; - } - } else if (encoding == SIMPLE.code) { - return firstLine.substring("Subject:".length()).trim(); - } else if (firstLine.length() > 50) { - return firstLine.substring(0, 50).trim() + "..."; - } else { - return firstLine; - } - } - - public String getText() { - if (encoding == EXTENDED.code) { - if (Message.TYPE.equals(getExtendedData().getType())) { - return ((Message) extendedData.getContent()).getBody(); - } else { - return null; - } - } else { - try { - String text = new String(message, "UTF-8"); - if (encoding == SIMPLE.code) { - return text.substring(text.indexOf("\nBody:") + 6); - } - return text; - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - } - } - - protected ExtendedEncoding getExtendedData() { - if (extendedData == null && encoding == EXTENDED.code) { - // TODO: make sure errors are properly handled - extendedData = ExtendedEncodingFactory.getInstance().unzip(message); - } - return extendedData; - } - - @SuppressWarnings("unchecked") - public <T extends ExtendedEncoding.ExtendedType> T getExtendedData(Class<T> type) { - ExtendedEncoding extendedData = getExtendedData(); - if (extendedData == null) { - return null; - } - if (type == null || type.isInstance(extendedData.getContent())) { - return (T) extendedData.getContent(); - } - return null; - } - - public List<InventoryVector> getParents() { - if (getExtendedData() != null && Message.TYPE.equals(getExtendedData().getType())) { - return ((Message) extendedData.getContent()).getParents(); - } else { - return Collections.emptyList(); - } - } - - public List<Attachment> getFiles() { - if (Message.TYPE.equals(getExtendedData().getType())) { - return ((Message) extendedData.getContent()).getFiles(); - } else { - return Collections.emptyList(); - } - } - - public UUID getConversationId() { - return conversationId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Plaintext plaintext = (Plaintext) o; - return Objects.equals(encoding, plaintext.encoding) && - Objects.equals(from, plaintext.from) && - Arrays.equals(message, plaintext.message) && - Objects.equals(getAckMessage(), plaintext.getAckMessage()) && - Arrays.equals(to == null ? null : to.getRipe(), plaintext.to == null ? null : plaintext.to.getRipe()) && - Arrays.equals(signature, plaintext.signature) && - Objects.equals(status, plaintext.status) && - Objects.equals(sent, plaintext.sent) && - Objects.equals(received, plaintext.received) && - Objects.equals(labels, plaintext.labels); - } - - @Override - public int hashCode() { - return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels); - } - - public void addLabels(Label... labels) { - if (labels != null) { - Collections.addAll(this.labels, labels); - } - } - - public void addLabels(Collection<Label> labels) { - if (labels != null) { - this.labels.addAll(labels); - } - } - - public void removeLabel(Label.Type type) { - Iterator<Label> iterator = labels.iterator(); - while (iterator.hasNext()) { - Label label = iterator.next(); - if (label.getType() == type) { - iterator.remove(); - } - } - } - - public byte[] getAckData() { - return ackData; - } - - public ObjectMessage getAckMessage() { - if (ackMessage == null) { - ackMessage = Factory.createAck(this); - } - return ackMessage; - } - - public void setInitialHash(byte[] initialHash) { - this.initialHash = initialHash; - } - - public byte[] getInitialHash() { - return initialHash; - } - - @Override - public String toString() { - String subject = getSubject(); - if (subject == null || subject.length() == 0) { - return Strings.hex(initialHash).toString(); - } else { - return subject; - } - } - - public enum Encoding { - IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3); - - long code; - - Encoding(long code) { - this.code = code; - } - - public long getCode() { - return code; - } - - public static Encoding fromCode(long code) { - for (Encoding e : values()) { - if (e.getCode() == code) { - return e; - } - } - return null; - } - } - - public enum Status { - DRAFT, - // For sent messages - PUBKEY_REQUESTED, - DOING_PROOF_OF_WORK, - SENT, - SENT_ACKNOWLEDGED, - RECEIVED - } - - public enum Type { - MSG, BROADCAST - } - - public static final class Builder { - private Object id; - private InventoryVector inventoryVector; - private Type type; - private BitmessageAddress from; - private BitmessageAddress to; - private long addressVersion; - private long stream; - private int behaviorBitfield; - private byte[] publicSigningKey; - private byte[] publicEncryptionKey; - private long nonceTrialsPerByte; - private long extraBytes; - private byte[] destinationRipe; - private long encoding; - private byte[] message = new byte[0]; - private byte[] ackData; - private byte[] ackMessage; - private byte[] signature; - private Long sent; - private Long received; - private Status status; - private Set<Label> labels = new LinkedHashSet<>(); - private long ttl; - private int retries; - private Long nextTry; - private UUID conversation; - - public Builder(Type type) { - this.type = type; - } - - public Builder id(Object id) { - this.id = id; - return this; - } - - public Builder IV(InventoryVector iv) { - this.inventoryVector = iv; - return this; - } - - public Builder from(BitmessageAddress address) { - from = address; - return this; - } - - public Builder to(BitmessageAddress address) { - if (type != Type.MSG && to != null) - throw new IllegalArgumentException("recipient address only allowed for msg"); - to = address; - return this; - } - - private Builder addressVersion(long addressVersion) { - this.addressVersion = addressVersion; - return this; - } - - private Builder stream(long stream) { - this.stream = stream; - return this; - } - - private Builder behaviorBitfield(int behaviorBitfield) { - this.behaviorBitfield = behaviorBitfield; - return this; - } - - private Builder publicSigningKey(byte[] publicSigningKey) { - this.publicSigningKey = publicSigningKey; - return this; - } - - private Builder publicEncryptionKey(byte[] publicEncryptionKey) { - this.publicEncryptionKey = publicEncryptionKey; - return this; - } - - private Builder nonceTrialsPerByte(long nonceTrialsPerByte) { - this.nonceTrialsPerByte = nonceTrialsPerByte; - return this; - } - - private Builder extraBytes(long extraBytes) { - this.extraBytes = extraBytes; - return this; - } - - private Builder destinationRipe(byte[] ripe) { - if (type != Type.MSG && ripe != null) throw new IllegalArgumentException("ripe only allowed for msg"); - this.destinationRipe = ripe; - return this; - } - - public Builder encoding(Encoding encoding) { - this.encoding = encoding.getCode(); - return this; - } - - private Builder encoding(long encoding) { - this.encoding = encoding; - return this; - } - - public Builder message(ExtendedEncoding message) { - this.encoding = EXTENDED.getCode(); - this.message = message.zip(); - return this; - } - - public Builder message(String subject, String message) { - try { - this.encoding = SIMPLE.getCode(); - this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - return this; - } - - public Builder message(byte[] message) { - this.message = message; - return this; - } - - public Builder ackMessage(byte[] ack) { - if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ackMessage only allowed for msg"); - this.ackMessage = ack; - return this; - } - - public Builder ackData(byte[] ackData) { - if (type != Type.MSG && ackData != null) - throw new IllegalArgumentException("ackMessage only allowed for msg"); - this.ackData = ackData; - return this; - } - - public Builder signature(byte[] signature) { - this.signature = signature; - return this; - } - - public Builder sent(Long sent) { - this.sent = sent; - return this; - } - - public Builder received(Long received) { - this.received = received; - return this; - } - - public Builder status(Status status) { - this.status = status; - return this; - } - - public Builder labels(Collection<Label> labels) { - this.labels.addAll(labels); - return this; - } - - public Builder ttl(long ttl) { - this.ttl = ttl; - return this; - } - - public Builder retries(int retries) { - this.retries = retries; - return this; - } - - public Builder nextTry(Long nextTry) { - this.nextTry = nextTry; - return this; - } - - public Builder conversation(UUID id) { - this.conversation = id; - return this; - } - - public Plaintext build() { - if (from == null) { - from = new BitmessageAddress(Factory.createPubkey( - addressVersion, - stream, - publicSigningKey, - publicEncryptionKey, - nonceTrialsPerByte, - extraBytes, - behaviorBitfield - )); - } - if (to == null && type != Type.BROADCAST && destinationRipe != null) { - to = new BitmessageAddress(0, 0, destinationRipe); - } - if (type == Type.MSG && ackMessage == null && ackData == null) { - ackData = cryptography().randomBytes(Msg.ACK_LENGTH); - } - if (ttl <= 0) { - ttl = TTL.msg(); - } - if (conversation == null) { - conversation = UUID.randomUUID(); - } - return new Plaintext(this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java b/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java deleted file mode 100644 index e75a926..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java +++ /dev/null @@ -1,31 +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.entity; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.Serializable; -import java.nio.ByteBuffer; - -/** - * An object that can be written to an {@link OutputStream} - */ -public interface Streamable extends Serializable { - void write(OutputStream stream) throws IOException; - - void write(ByteBuffer buffer); -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Version.java b/core/src/main/java/ch/dissem/bitmessage/entity/Version.java deleted file mode 100644 index bfb4869..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Version.java +++ /dev/null @@ -1,246 +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.entity; - -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.UnixTime; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * The 'version' command advertises this node's latest supported protocol version upon initiation. - */ -public class Version implements MessagePayload { - private static final long serialVersionUID = 7219240857343176567L; - - /** - * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's - * version is lower but continue with the connection if it is higher. - */ - private final int version; - - /** - * bitfield of features to be enabled for this connection - */ - private final long services; - - /** - * standard UNIX timestamp in seconds - */ - private final long timestamp; - - /** - * The network address of the node receiving this message (not including the time or stream number) - */ - private final NetworkAddress addrRecv; - - /** - * The network address of the node emitting this message (not including the time or stream number and the ip itself - * is ignored by the receiver) - */ - private final NetworkAddress addrFrom; - - /** - * Random nonce used to detect connections to self. - */ - private final long nonce; - - /** - * User Agent (0x00 if string is 0 bytes long). Sending nodes must not include a user_agent longer than 5000 bytes. - */ - private final String userAgent; - - /** - * The stream numbers that the emitting node is interested in. Sending nodes must not include more than 160000 - * stream numbers. - */ - private final long[] streams; - - private Version(Builder builder) { - version = builder.version; - services = builder.services; - timestamp = builder.timestamp; - addrRecv = builder.addrRecv; - addrFrom = builder.addrFrom; - nonce = builder.nonce; - userAgent = builder.userAgent; - streams = builder.streamNumbers; - } - - public int getVersion() { - return version; - } - - public long getServices() { - return services; - } - - public boolean provides(Service service) { - return service != null && service.isEnabled(services); - } - - public long getTimestamp() { - return timestamp; - } - - public NetworkAddress getAddrRecv() { - return addrRecv; - } - - public NetworkAddress getAddrFrom() { - return addrFrom; - } - - public long getNonce() { - return nonce; - } - - public String getUserAgent() { - return userAgent; - } - - public long[] getStreams() { - return streams; - } - - @Override - public Command getCommand() { - return Command.VERSION; - } - - @Override - public void write(OutputStream stream) throws IOException { - Encode.int32(version, stream); - Encode.int64(services, stream); - Encode.int64(timestamp, stream); - addrRecv.write(stream, true); - addrFrom.write(stream, true); - Encode.int64(nonce, stream); - Encode.varString(userAgent, stream); - Encode.varIntList(streams, stream); - } - - @Override - public void write(ByteBuffer buffer) { - Encode.int32(version, buffer); - Encode.int64(services, buffer); - Encode.int64(timestamp, buffer); - addrRecv.write(buffer, true); - addrFrom.write(buffer, true); - Encode.int64(nonce, buffer); - Encode.varString(userAgent, buffer); - Encode.varIntList(streams, buffer); - } - - - public static final class Builder { - private int version; - private long services; - private long timestamp; - private NetworkAddress addrRecv; - private NetworkAddress addrFrom; - private long nonce; - private String userAgent; - private long[] streamNumbers; - - public Builder defaults(long clientNonce) { - version = BitmessageContext.CURRENT_VERSION; - services = Service.getServiceFlag(Service.NODE_NETWORK); - timestamp = UnixTime.now(); - userAgent = "/Jabit:0.0.1/"; - streamNumbers = new long[]{1}; - nonce = clientNonce; - return this; - } - - public Builder version(int version) { - this.version = version; - return this; - } - - public Builder services(Service... services) { - this.services = Service.getServiceFlag(services); - return this; - } - - public Builder services(long services) { - this.services = services; - return this; - } - - public Builder timestamp(long timestamp) { - this.timestamp = timestamp; - return this; - } - - public Builder addrRecv(NetworkAddress addrRecv) { - this.addrRecv = addrRecv; - return this; - } - - public Builder addrFrom(NetworkAddress addrFrom) { - this.addrFrom = addrFrom; - return this; - } - - public Builder nonce(long nonce) { - this.nonce = nonce; - return this; - } - - public Builder userAgent(String userAgent) { - this.userAgent = userAgent; - return this; - } - - public Builder streams(long... streamNumbers) { - this.streamNumbers = streamNumbers; - return this; - } - - public Version build() { - return new Version(this); - } - } - - public enum Service { - NODE_NETWORK(1); -// TODO: NODE_SSL(2); - - long flag; - - Service(long flag) { - this.flag = flag; - } - - public boolean isEnabled(long flag) { - return (flag & this.flag) != 0; - } - - public static long getServiceFlag(Service... services) { - long flag = 0; - for (Service service : services) { - flag |= service.flag; - } - return flag; - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java deleted file mode 100644 index a583330..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java +++ /dev/null @@ -1,114 +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.entity.payload; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -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 java.io.IOException; -import java.util.Objects; - -import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * Users who are subscribed to the sending address will see the message appear in their inbox. - * Broadcasts are version 4 or 5. - */ -public abstract class Broadcast extends ObjectPayload implements Encrypted, PlaintextHolder { - private static final long serialVersionUID = 4064521827582239069L; - - protected final long stream; - protected CryptoBox encrypted; - protected Plaintext plaintext; - - protected Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) { - super(version); - this.stream = stream; - this.encrypted = encrypted; - this.plaintext = plaintext; - } - - public static long getVersion(BitmessageAddress address) { - return address.getVersion() < 4 ? 4 : 5; - } - - @Override - public boolean isSigned() { - return true; - } - - @Override - public byte[] getSignature() { - return plaintext.getSignature(); - } - - @Override - public void setSignature(byte[] signature) { - plaintext.setSignature(signature); - } - - @Override - public long getStream() { - return stream; - } - - @Override - public Plaintext getPlaintext() { - return plaintext; - } - - @Override - public void encrypt(byte[] publicKey) throws IOException { - this.encrypted = new CryptoBox(plaintext, publicKey); - } - - public void encrypt() throws IOException { - encrypt(cryptography().createPublicKey(plaintext.getFrom().getPublicDecryptionKey())); - } - - @Override - public void decrypt(byte[] privateKey) throws IOException, DecryptionFailedException { - plaintext = Plaintext.read(BROADCAST, encrypted.decrypt(privateKey)); - } - - public void decrypt(BitmessageAddress address) throws IOException, DecryptionFailedException { - decrypt(address.getPublicDecryptionKey()); - } - - @Override - public boolean isDecrypted() { - return plaintext != null; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Broadcast broadcast = (Broadcast) o; - return stream == broadcast.stream && - (Objects.equals(encrypted, broadcast.encrypted) || Objects.equals(plaintext, broadcast.plaintext)); - } - - @Override - public int hashCode() { - return Objects.hash(stream); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java deleted file mode 100644 index f7f3c15..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java +++ /dev/null @@ -1,214 +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.entity.payload; - -import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.utils.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.nio.ByteBuffer; -import java.util.Arrays; - -import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - - -public class CryptoBox implements Streamable { - private static final long serialVersionUID = 7217659539975573852L; - private static final Logger LOG = LoggerFactory.getLogger(CryptoBox.class); - - private final byte[] initializationVector; - private final int curveType; - private final byte[] R; - private final byte[] mac; - private byte[] encrypted; - - - public CryptoBox(Streamable data, byte[] K) throws IOException { - this(Encode.bytes(data), K); - } - - public CryptoBox(byte[] data, byte[] K) throws IOException { - curveType = 0x02CA; - - // 1. The destination public key is called K. - // 2. Generate 16 random bytes using a secure random number generator. Call them IV. - initializationVector = cryptography().randomBytes(16); - - // 3. Generate a new random EC key pair with private key called r and public key called R. - byte[] r = cryptography().randomBytes(PRIVATE_KEY_SIZE); - R = cryptography().createPublicKey(r); - // 4. Do an EC point multiply with public key K and private key r. This gives you public key P. - byte[] P = cryptography().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 = cryptography().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 = cryptography().crypt(true, data, key_e, initializationVector); - // 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. - mac = calculateMac(key_m); - - // The resulting data is: IV + R + cipher text + MAC - } - - private CryptoBox(Builder builder) { - initializationVector = builder.initializationVector; - curveType = builder.curveType; - R = cryptography().createPoint(builder.xComponent, builder.yComponent); - encrypted = builder.encrypted; - mac = builder.mac; - } - - public static CryptoBox read(InputStream stream, int length) throws IOException { - AccessCounter counter = new AccessCounter(); - return new Builder() - .IV(Decode.bytes(stream, 16, counter)) - .curveType(Decode.uint16(stream, counter)) - .X(Decode.shortVarBytes(stream, counter)) - .Y(Decode.shortVarBytes(stream, counter)) - .encrypted(Decode.bytes(stream, length - counter.length() - 32)) - .MAC(Decode.bytes(stream, 32)) - .build(); - } - - /** - * @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[] k) throws DecryptionFailedException { - // 1. The private key used to decrypt is called k. - // 2. Do an EC point multiply with private key k and public key R. This gives you public key P. - byte[] P = cryptography().multiply(R, k); - // 3. Use the X component of public key P and calculate the SHA512 hash H. - byte[] H = cryptography().sha512(Arrays.copyOfRange(P, 1, 33)); - // 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. - byte[] key_e = Arrays.copyOfRange(H, 0, 32); - byte[] key_m = Arrays.copyOfRange(H, 32, 64); - - // 5. Calculate MAC' with HMACSHA256, using key_m as salt and IV + R + cipher text as data. - // 6. Compare MAC with MAC'. If not equal, decryption will fail. - if (!Arrays.equals(mac, calculateMac(key_m))) { - throw new DecryptionFailedException(); - } - - // 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(cryptography().crypt(false, encrypted, key_e, initializationVector)); - } - - private byte[] calculateMac(byte[] key_m) { - try { - ByteArrayOutputStream macData = new ByteArrayOutputStream(); - writeWithoutMAC(macData); - return cryptography().mac(key_m, macData.toByteArray()); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - private void writeWithoutMAC(OutputStream out) throws IOException { - out.write(initializationVector); - Encode.int16(curveType, out); - writeCoordinateComponent(out, Points.getX(R)); - writeCoordinateComponent(out, Points.getY(R)); - out.write(encrypted); - } - - private void writeCoordinateComponent(OutputStream out, byte[] x) throws IOException { - int offset = Bytes.numberOfLeadingZeros(x); - int length = x.length - offset; - Encode.int16(length, out); - out.write(x, offset, length); - } - - private void writeCoordinateComponent(ByteBuffer buffer, byte[] x) { - int offset = Bytes.numberOfLeadingZeros(x); - int length = x.length - offset; - Encode.int16(length, buffer); - buffer.put(x, offset, length); - } - - @Override - public void write(OutputStream stream) throws IOException { - writeWithoutMAC(stream); - stream.write(mac); - } - - @Override - public void write(ByteBuffer buffer) { - buffer.put(initializationVector); - Encode.int16(curveType, buffer); - writeCoordinateComponent(buffer, Points.getX(R)); - writeCoordinateComponent(buffer, Points.getY(R)); - buffer.put(encrypted); - buffer.put(mac); - } - - public static final class Builder { - private byte[] initializationVector; - private int curveType; - private byte[] xComponent; - private byte[] yComponent; - private byte[] encrypted; - private byte[] mac; - - public Builder IV(byte[] initializationVector) { - this.initializationVector = initializationVector; - return this; - } - - public Builder curveType(int curveType) { - if (curveType != 0x2CA) LOG.trace("Unexpected curve type " + curveType); - this.curveType = curveType; - return this; - } - - public Builder X(byte[] xComponent) { - this.xComponent = xComponent; - return this; - } - - public Builder Y(byte[] yComponent) { - this.yComponent = yComponent; - return this; - } - - private Builder encrypted(byte[] encrypted) { - this.encrypted = encrypted; - return this; - } - - public Builder MAC(byte[] mac) { - this.mac = mac; - return this; - } - - public CryptoBox build() { - return new CryptoBox(this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java deleted file mode 100644 index 9312160..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java +++ /dev/null @@ -1,88 +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.entity.payload; - -import ch.dissem.bitmessage.utils.Decode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; - -/** - * In cases we don't know what to do with an object, we just store its bytes and send it again - we don't really - * have to know what it is. - */ -public class GenericPayload extends ObjectPayload { - private static final long serialVersionUID = -912314085064185940L; - - private long stream; - private byte[] data; - - public GenericPayload(long version, long stream, byte[] data) { - super(version); - this.stream = stream; - this.data = data; - } - - public static GenericPayload read(long version, long stream, InputStream is, int length) throws IOException { - return new GenericPayload(version, stream, Decode.bytes(is, length)); - } - - @Override - public ObjectType getType() { - return null; - } - - @Override - public long getStream() { - return stream; - } - - public byte[] getData() { - return data; - } - - @Override - public void write(OutputStream stream) throws IOException { - stream.write(data); - } - - @Override - public void write(ByteBuffer buffer) { - buffer.put(data); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - GenericPayload that = (GenericPayload) o; - - if (stream != that.stream) return false; - return Arrays.equals(data, that.data); - } - - @Override - public int hashCode() { - int result = (int) (stream ^ (stream >>> 32)); - result = 31 * result + Arrays.hashCode(data); - return result; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java deleted file mode 100644 index d889489..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java +++ /dev/null @@ -1,82 +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.entity.payload; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.utils.Decode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * Request for a public key. - */ -public class GetPubkey extends ObjectPayload { - private static final long serialVersionUID = -3634516646972610180L; - - private long stream; - private byte[] ripeTag; - - public GetPubkey(BitmessageAddress address) { - super(address.getVersion()); - this.stream = address.getStream(); - if (address.getVersion() < 4) - this.ripeTag = address.getRipe(); - else - this.ripeTag = address.getTag(); - } - - private GetPubkey(long version, long stream, byte[] ripeOrTag) { - super(version); - this.stream = stream; - this.ripeTag = ripeOrTag; - } - - public static GetPubkey read(InputStream is, long stream, int length, long version) throws IOException { - return new GetPubkey(version, stream, Decode.bytes(is, length)); - } - - /** - * @return an array of bytes that represent either the ripe, or the tag of an address, depending on the - * address version. - */ - public byte[] getRipeTag() { - return ripeTag; - } - - @Override - public ObjectType getType() { - return ObjectType.GET_PUBKEY; - } - - @Override - public long getStream() { - return stream; - } - - @Override - public void write(OutputStream stream) throws IOException { - stream.write(ripeTag); - } - - @Override - public void write(ByteBuffer buffer) { - buffer.put(ripeTag); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java deleted file mode 100644 index dc36bb1..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java +++ /dev/null @@ -1,135 +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.entity.payload; - -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 java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Objects; - -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; - -/** - * Used for person-to-person messages. - */ -public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder { - private static final long serialVersionUID = 4327495048296365733L; - public static final int ACK_LENGTH = 32; - - private long stream; - private CryptoBox encrypted; - private Plaintext plaintext; - - private Msg(long stream, CryptoBox encrypted) { - super(1); - this.stream = stream; - this.encrypted = encrypted; - } - - public Msg(Plaintext plaintext) { - super(1); - this.stream = plaintext.getStream(); - this.plaintext = plaintext; - } - - public static Msg read(InputStream in, long stream, int length) throws IOException { - return new Msg(stream, CryptoBox.read(in, length)); - } - - @Override - public Plaintext getPlaintext() { - return plaintext; - } - - @Override - public ObjectType getType() { - return ObjectType.MSG; - } - - @Override - public long getStream() { - return stream; - } - - @Override - public boolean isSigned() { - return true; - } - - @Override - public void writeBytesToSign(OutputStream out) throws IOException { - plaintext.write(out, false); - } - - @Override - public byte[] getSignature() { - return plaintext.getSignature(); - } - - @Override - public void setSignature(byte[] signature) { - plaintext.setSignature(signature); - } - - @Override - public void encrypt(byte[] publicKey) throws IOException { - this.encrypted = new CryptoBox(plaintext, publicKey); - } - - @Override - public void decrypt(byte[] privateKey) throws IOException, DecryptionFailedException { - plaintext = Plaintext.read(MSG, encrypted.decrypt(privateKey)); - } - - @Override - public boolean isDecrypted() { - return plaintext != null; - } - - @Override - public void write(OutputStream out) throws IOException { - if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it."); - encrypted.write(out); - } - - @Override - public void write(ByteBuffer buffer) { - if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it."); - encrypted.write(buffer); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - Msg msg = (Msg) o; - return stream == msg.stream && - (Objects.equals(encrypted, msg.encrypted) || Objects.equals(plaintext, msg.plaintext)); - } - - @Override - public int hashCode() { - return (int) stream; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java deleted file mode 100644 index 33da28d..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java +++ /dev/null @@ -1,66 +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.entity.payload; - -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Streamable; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * The payload of an 'object' command. This is shared by the network. - */ -public abstract class ObjectPayload implements Streamable { - private static final long serialVersionUID = -5034977402902364482L; - - private final long version; - - protected ObjectPayload(long version) { - this.version = version; - } - - - public abstract ObjectType getType(); - - public abstract long getStream(); - - public long getVersion() { - return version; - } - - public boolean isSigned() { - return false; - } - - public void writeBytesToSign(OutputStream out) throws IOException { - // nothing to do - } - - /** - * @return the ECDSA signature which, as of protocol v3, covers the object header starting with the time, - * appended with the data described in this table down to the extra_bytes. Therefore, this must - * be checked and set in the {@link ObjectMessage} object. - */ - public byte[] getSignature() { - return null; - } - - public void setSignature(byte[] signature) { - // nothing to do - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java deleted file mode 100644 index 27d2da9..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java +++ /dev/null @@ -1,121 +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.entity.payload; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.ArrayList; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * Public keys for signing and encryption, the answer to a 'getpubkey' request. - */ -public abstract class Pubkey extends ObjectPayload { - private static final long serialVersionUID = -6634533361454999619L; - - public final static long LATEST_VERSION = 4; - - protected Pubkey(long version) { - super(version); - } - - public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) { - return cryptography().ripemd160(cryptography().sha512(publicSigningKey, publicEncryptionKey)); - } - - public abstract byte[] getSigningKey(); - - public abstract byte[] getEncryptionKey(); - - public abstract int getBehaviorBitfield(); - - public byte[] getRipe() { - return cryptography().ripemd160(cryptography().sha512(getSigningKey(), getEncryptionKey())); - } - - public long getNonceTrialsPerByte() { - return 0; - } - - public long getExtraBytes() { - return 0; - } - - public void writeUnencrypted(OutputStream out) throws IOException { - write(out); - } - - public void writeUnencrypted(ByteBuffer buffer){ - write(buffer); - } - - protected byte[] add0x04(byte[] key) { - if (key.length == 65) return key; - byte[] result = new byte[65]; - result[0] = 4; - System.arraycopy(key, 0, result, 1, 64); - return result; - } - - /** - * Bits 0 through 29 are yet undefined - */ - public enum Feature { - /** - * Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg - * messages bound for them. - */ - INCLUDE_DESTINATION(30), - /** - * If true, the receiving node does send acknowledgements (rather than dropping them). - */ - DOES_ACK(31); - - private int bit; - - Feature(int bitNumber) { - // The Bitmessage Protocol Specification starts counting at the most significant bit, - // thus the slightly awkward calculation. - // https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features - this.bit = 1 << (31 - bitNumber); - } - - public static int bitfield(Feature... features) { - int bits = 0; - for (Feature feature : features) { - bits |= feature.bit; - } - return bits; - } - - public static Feature[] features(int bitfield) { - ArrayList<Feature> features = new ArrayList<>(Feature.values().length); - for (Feature feature : Feature.values()) { - if ((bitfield & feature.bit) != 0) { - features.add(feature); - } - } - return features.toArray(new Feature[features.size()]); - } - - public boolean isActive(int bitfield) { - return (bitfield & bit) != 0; - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java deleted file mode 100644 index d2901c1..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java +++ /dev/null @@ -1,133 +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.entity.payload; - -import ch.dissem.bitmessage.utils.Decode; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * A version 2 public key. - */ -public class V2Pubkey extends Pubkey { - private static final long serialVersionUID = -257598690676510460L; - - protected long stream; - protected int behaviorBitfield; - protected byte[] publicSigningKey; // 64 Bytes - protected byte[] publicEncryptionKey; // 64 Bytes - - protected V2Pubkey(long version) { - super(version); - } - - private V2Pubkey(long version, Builder builder) { - super(version); - stream = builder.streamNumber; - behaviorBitfield = builder.behaviorBitfield; - publicSigningKey = add0x04(builder.publicSigningKey); - publicEncryptionKey = add0x04(builder.publicEncryptionKey); - } - - public static V2Pubkey read(InputStream is, long stream) throws IOException { - return new V2Pubkey.Builder() - .stream(stream) - .behaviorBitfield((int) Decode.uint32(is)) - .publicSigningKey(Decode.bytes(is, 64)) - .publicEncryptionKey(Decode.bytes(is, 64)) - .build(); - } - - @Override - public long getVersion() { - return 2; - } - - @Override - public ObjectType getType() { - return ObjectType.PUBKEY; - } - - @Override - public long getStream() { - return stream; - } - - @Override - public byte[] getSigningKey() { - return publicSigningKey; - } - - @Override - public byte[] getEncryptionKey() { - return publicEncryptionKey; - } - - @Override - public int getBehaviorBitfield() { - return behaviorBitfield; - } - - @Override - public void write(OutputStream out) throws IOException { - Encode.int32(behaviorBitfield, out); - out.write(publicSigningKey, 1, 64); - out.write(publicEncryptionKey, 1, 64); - } - - @Override - public void write(ByteBuffer buffer) { - Encode.int32(behaviorBitfield, buffer); - buffer.put(publicSigningKey, 1, 64); - buffer.put(publicEncryptionKey, 1, 64); - } - - public static class Builder { - private long streamNumber; - private int behaviorBitfield; - private byte[] publicSigningKey; - private byte[] publicEncryptionKey; - - public Builder stream(long streamNumber) { - this.streamNumber = streamNumber; - return this; - } - - public Builder behaviorBitfield(int behaviorBitfield) { - this.behaviorBitfield = behaviorBitfield; - return this; - } - - public Builder publicSigningKey(byte[] publicSigningKey) { - this.publicSigningKey = publicSigningKey; - return this; - } - - public Builder publicEncryptionKey(byte[] publicEncryptionKey) { - this.publicEncryptionKey = publicEncryptionKey; - return this; - } - - public V2Pubkey build() { - return new V2Pubkey(2, this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java deleted file mode 100644 index 5260060..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java +++ /dev/null @@ -1,175 +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.entity.payload; - -import ch.dissem.bitmessage.utils.Decode; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Objects; - -/** - * A version 3 public key. - */ -public class V3Pubkey extends V2Pubkey { - private static final long serialVersionUID = 6958853116648528319L; - - long nonceTrialsPerByte; - long extraBytes; - byte[] signature; - - protected V3Pubkey(long version, Builder builder) { - super(version); - stream = builder.streamNumber; - behaviorBitfield = builder.behaviorBitfield; - publicSigningKey = add0x04(builder.publicSigningKey); - publicEncryptionKey = add0x04(builder.publicEncryptionKey); - nonceTrialsPerByte = builder.nonceTrialsPerByte; - extraBytes = builder.extraBytes; - signature = builder.signature; - } - - public static V3Pubkey read(InputStream is, long stream) throws IOException { - return new V3Pubkey.Builder() - .stream(stream) - .behaviorBitfield(Decode.int32(is)) - .publicSigningKey(Decode.bytes(is, 64)) - .publicEncryptionKey(Decode.bytes(is, 64)) - .nonceTrialsPerByte(Decode.varInt(is)) - .extraBytes(Decode.varInt(is)) - .signature(Decode.varBytes(is)) - .build(); - } - - @Override - public void write(OutputStream out) throws IOException { - writeBytesToSign(out); - Encode.varBytes(signature, out); - } - - @Override - public void write(ByteBuffer buffer) { - super.write(buffer); - Encode.varInt(nonceTrialsPerByte, buffer); - Encode.varInt(extraBytes, buffer); - Encode.varBytes(signature, buffer); - } - - @Override - public long getVersion() { - return 3; - } - - public long getNonceTrialsPerByte() { - return nonceTrialsPerByte; - } - - public long getExtraBytes() { - return extraBytes; - } - - public boolean isSigned() { - return true; - } - - public void writeBytesToSign(OutputStream out) throws IOException { - super.write(out); - Encode.varInt(nonceTrialsPerByte, out); - Encode.varInt(extraBytes, out); - } - - @Override - public byte[] getSignature() { - return signature; - } - - @Override - public void setSignature(byte[] signature) { - this.signature = signature; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - V3Pubkey pubkey = (V3Pubkey) o; - return Objects.equals(nonceTrialsPerByte, pubkey.nonceTrialsPerByte) && - Objects.equals(extraBytes, pubkey.extraBytes) && - stream == pubkey.stream && - behaviorBitfield == pubkey.behaviorBitfield && - Arrays.equals(publicSigningKey, pubkey.publicSigningKey) && - Arrays.equals(publicEncryptionKey, pubkey.publicEncryptionKey); - } - - @Override - public int hashCode() { - return Objects.hash(nonceTrialsPerByte, extraBytes); - } - - public static class Builder { - private long streamNumber; - private int behaviorBitfield; - private byte[] publicSigningKey; - private byte[] publicEncryptionKey; - private long nonceTrialsPerByte; - private long extraBytes; - private byte[] signature = new byte[0]; - - public Builder stream(long streamNumber) { - this.streamNumber = streamNumber; - return this; - } - - public Builder behaviorBitfield(int behaviorBitfield) { - this.behaviorBitfield = behaviorBitfield; - return this; - } - - public Builder publicSigningKey(byte[] publicSigningKey) { - this.publicSigningKey = publicSigningKey; - return this; - } - - public Builder publicEncryptionKey(byte[] publicEncryptionKey) { - this.publicEncryptionKey = publicEncryptionKey; - return this; - } - - public Builder nonceTrialsPerByte(long nonceTrialsPerByte) { - this.nonceTrialsPerByte = nonceTrialsPerByte; - return this; - } - - public Builder extraBytes(long extraBytes) { - this.extraBytes = extraBytes; - return this; - } - - public Builder signature(byte[] signature) { - this.signature = signature; - return this; - } - - public V3Pubkey build() { - return new V3Pubkey(3, this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java deleted file mode 100644 index 323da33..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java +++ /dev/null @@ -1,67 +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.entity.payload; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * Users who are subscribed to the sending address will see the message appear in their inbox. - * Broadcasts are version 4 or 5. - */ -public class V4Broadcast extends Broadcast { - private static final long serialVersionUID = 195663108282762711L; - - protected V4Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) { - super(version, stream, encrypted, plaintext); - } - - public V4Broadcast(BitmessageAddress senderAddress, Plaintext plaintext) { - super(4, senderAddress.getStream(), null, plaintext); - if (senderAddress.getVersion() >= 4) - throw new IllegalArgumentException("Address version 3 or older expected, but was " + senderAddress.getVersion()); - } - - public static V4Broadcast read(InputStream in, long stream, int length) throws IOException { - return new V4Broadcast(4, stream, CryptoBox.read(in, length), null); - } - - @Override - public ObjectType getType() { - return ObjectType.BROADCAST; - } - - @Override - public void writeBytesToSign(OutputStream out) throws IOException { - plaintext.write(out, false); - } - - @Override - public void write(OutputStream out) throws IOException { - encrypted.write(out); - } - - @Override - public void write(ByteBuffer buffer) { - encrypted.write(buffer); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java deleted file mode 100644 index 179a475..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java +++ /dev/null @@ -1,191 +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.entity.payload; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Encrypted; -import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.utils.Decode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; - -/** - * A version 4 public key. When version 4 pubkeys are created, most of the data in the pubkey is encrypted. This is - * done in such a way that only someone who has the Bitmessage address which corresponds to a pubkey can decrypt and - * use that pubkey. This prevents people from gathering pubkeys sent around the network and using the data from them - * to create messages to be used in spam or in flooding attacks. - */ -public class V4Pubkey extends Pubkey implements Encrypted { - private static final long serialVersionUID = 1556710353694033093L; - - private long stream; - private byte[] tag; - private CryptoBox encrypted; - private V3Pubkey decrypted; - - private V4Pubkey(long stream, byte[] tag, CryptoBox encrypted) { - super(4); - this.stream = stream; - this.tag = tag; - this.encrypted = encrypted; - } - - public V4Pubkey(V3Pubkey decrypted) { - super(4); - this.decrypted = decrypted; - this.stream = decrypted.stream; - this.tag = BitmessageAddress.calculateTag(4, decrypted.getStream(), decrypted.getRipe()); - } - - public static V4Pubkey read(InputStream in, long stream, int length, boolean encrypted) throws IOException { - if (encrypted) - return new V4Pubkey(stream, - Decode.bytes(in, 32), - CryptoBox.read(in, length - 32)); - else - return new V4Pubkey(V3Pubkey.read(in, stream)); - } - - @Override - public void encrypt(byte[] publicKey) throws IOException { - if (getSignature() == null) throw new IllegalStateException("Pubkey must be signed before encryption."); - this.encrypted = new CryptoBox(decrypted, publicKey); - } - - @Override - public void decrypt(byte[] privateKey) throws IOException, DecryptionFailedException { - decrypted = V3Pubkey.read(encrypted.decrypt(privateKey), stream); - } - - @Override - public boolean isDecrypted() { - return decrypted != null; - } - - @Override - public void write(OutputStream stream) throws IOException { - stream.write(tag); - encrypted.write(stream); - } - - @Override - public void write(ByteBuffer buffer) { - buffer.put(tag); - encrypted.write(buffer); - } - - @Override - public void writeUnencrypted(OutputStream out) throws IOException { - decrypted.write(out); - } - - @Override - public void writeUnencrypted(ByteBuffer buffer) { - decrypted.write(buffer); - } - - @Override - public void writeBytesToSign(OutputStream out) throws IOException { - out.write(tag); - decrypted.writeBytesToSign(out); - } - - @Override - public long getVersion() { - return 4; - } - - @Override - public ObjectType getType() { - return ObjectType.PUBKEY; - } - - @Override - public long getStream() { - return stream; - } - - public byte[] getTag() { - return tag; - } - - @Override - public byte[] getSigningKey() { - return decrypted.getSigningKey(); - } - - @Override - public byte[] getEncryptionKey() { - return decrypted.getEncryptionKey(); - } - - @Override - public int getBehaviorBitfield() { - return decrypted.getBehaviorBitfield(); - } - - @Override - public byte[] getSignature() { - if (decrypted != null) - return decrypted.getSignature(); - else - return null; - } - - @Override - public void setSignature(byte[] signature) { - decrypted.setSignature(signature); - } - - @Override - public boolean isSigned() { - return true; - } - - public long getNonceTrialsPerByte() { - return decrypted.getNonceTrialsPerByte(); - } - - public long getExtraBytes() { - return decrypted.getExtraBytes(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - V4Pubkey v4Pubkey = (V4Pubkey) o; - - if (stream != v4Pubkey.stream) return false; - if (!Arrays.equals(tag, v4Pubkey.tag)) return false; - return !(decrypted != null ? !decrypted.equals(v4Pubkey.decrypted) : v4Pubkey.decrypted != null); - - } - - @Override - public int hashCode() { - int result = (int) (stream ^ (stream >>> 32)); - result = 31 * result + Arrays.hashCode(tag); - result = 31 * result + (decrypted != null ? decrypted.hashCode() : 0); - return result; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java deleted file mode 100644 index 8f07a30..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java +++ /dev/null @@ -1,66 +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.entity.payload; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.utils.Decode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Users who are subscribed to the sending address will see the message appear in their inbox. - */ -public class V5Broadcast extends V4Broadcast { - private static final long serialVersionUID = 920649721626968644L; - - private byte[] tag; - - private V5Broadcast(long stream, byte[] tag, CryptoBox encrypted) { - super(5, stream, encrypted, null); - this.tag = tag; - } - - public V5Broadcast(BitmessageAddress senderAddress, Plaintext plaintext) { - super(5, senderAddress.getStream(), null, plaintext); - if (senderAddress.getVersion() < 4) - throw new IllegalArgumentException("Address version 4 (or newer) expected, but was " + senderAddress.getVersion()); - this.tag = senderAddress.getTag(); - } - - public static V5Broadcast read(InputStream is, long stream, int length) throws IOException { - return new V5Broadcast(stream, Decode.bytes(is, 32), CryptoBox.read(is, length - 32)); - } - - public byte[] getTag() { - return tag; - } - - @Override - public void writeBytesToSign(OutputStream out) throws IOException { - out.write(tag); - super.writeBytesToSign(out); - } - - @Override - public void write(OutputStream out) throws IOException { - out.write(tag); - super.write(out); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java deleted file mode 100644 index 3f3d1ec..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java +++ /dev/null @@ -1,76 +0,0 @@ -package ch.dissem.bitmessage.entity.valueobject; - -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.msgpack.types.MPMap; -import ch.dissem.msgpack.types.MPString; -import ch.dissem.msgpack.types.MPType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.Serializable; -import java.util.Objects; -import java.util.zip.DeflaterOutputStream; - -/** - * Extended encoding message object. - */ -public class ExtendedEncoding implements Serializable { - private static final long serialVersionUID = 3876871488247305200L; - private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncoding.class); - - private ExtendedType content; - - public ExtendedEncoding(ExtendedType content) { - this.content = content; - } - - public String getType() { - if (content == null) { - return null; - } else { - return content.getType(); - } - } - - public ExtendedType getContent() { - return content; - } - - public byte[] zip() { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - try (DeflaterOutputStream zipper = new DeflaterOutputStream(out)) { - content.pack().pack(zipper); - } - return out.toByteArray(); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ExtendedEncoding that = (ExtendedEncoding) o; - return Objects.equals(content, that.content); - } - - @Override - public int hashCode() { - return Objects.hash(content); - } - - public interface Unpacker<T extends ExtendedType> { - String getType(); - - T unpack(MPMap<MPString, MPType<?>> map); - } - - public interface ExtendedType extends Serializable { - String getType(); - - MPMap<MPString, MPType<?>> pack() throws IOException; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java deleted file mode 100644 index e1f8f60..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java +++ /dev/null @@ -1,82 +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.entity.valueobject; - -import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.utils.Strings; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.Serializable; -import java.nio.ByteBuffer; -import java.util.Arrays; - -public class InventoryVector implements Streamable, Serializable { - private static final long serialVersionUID = -7349009673063348719L; - - /** - * Hash of the object - */ - private final byte[] hash; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof InventoryVector)) return false; - - InventoryVector that = (InventoryVector) o; - - return Arrays.equals(hash, that.hash); - } - - @Override - public int hashCode() { - return hash == null ? 0 : Arrays.hashCode(hash); - } - - public byte[] getHash() { - return hash; - } - - private InventoryVector(byte[] hash) { - if (hash == null) throw new IllegalArgumentException("hash must not be null"); - this.hash = hash; - } - - public static InventoryVector fromHash(byte[] hash) { - if (hash == null) { - return null; - } else { - return new InventoryVector(hash); - } - } - - @Override - public void write(OutputStream out) throws IOException { - out.write(hash); - } - - @Override - public void write(ByteBuffer buffer) { - buffer.put(hash); - } - - @Override - public String toString() { - return Strings.hex(hash).toString(); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java deleted file mode 100644 index facbe55..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java +++ /dev/null @@ -1,89 +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.entity.valueobject; - -import java.io.Serializable; -import java.util.Objects; - -public class Label implements Serializable { - private static final long serialVersionUID = 831782893630994914L; - - private Object id; - private String label; - private Type type; - private int color; - - public Label(String label, Type type, int color) { - this.label = label; - this.type = type; - this.color = color; - } - - /** - * @return RGBA representation for the color. - */ - public int getColor() { - return color; - } - - /** - * @param color RGBA representation for the color. - */ - public void setColor(int color) { - this.color = color; - } - - @Override - public String toString() { - return label; - } - - public Object getId() { - return id; - } - - public void setId(Object id) { - this.id = id; - } - - public Type getType() { - return type; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Label label1 = (Label) o; - return Objects.equals(label, label1.label); - } - - @Override - public int hashCode() { - return Objects.hash(label); - } - - public enum Type { - INBOX, - BROADCAST, - DRAFT, - OUTBOX, - SENT, - UNREAD, - TRASH - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java deleted file mode 100644 index fe3bfcb..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java +++ /dev/null @@ -1,246 +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.entity.valueobject; - -import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.entity.Version; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.UnixTime; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.Arrays; - -/** - * A node's address. It's written in IPv6 format. - */ -public class NetworkAddress implements Streamable { - private static final long serialVersionUID = 2500120578167100300L; - - private long time; - - /** - * Stream number for this node - */ - private final long stream; - - /** - * same service(s) listed in version - */ - private final long services; - - /** - * IPv6 address. IPv4 addresses are written into the message as a 16 byte IPv4-mapped IPv6 address - * (12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of the IPv4 address). - */ - private final byte[] ipv6; - private final int port; - - private NetworkAddress(Builder builder) { - time = builder.time; - stream = builder.stream; - services = builder.services; - ipv6 = builder.ipv6; - port = builder.port; - } - - public byte[] getIPv6() { - return ipv6; - } - - public int getPort() { - return port; - } - - public long getServices() { - return services; - } - - public boolean provides(Version.Service service) { - if (service == null) { - return false; - } - return service.isEnabled(services); - } - - public long getStream() { - return stream; - } - - public long getTime() { - return time; - } - - public void setTime(long time) { - this.time = time; - } - - public InetAddress toInetAddress() { - try { - return InetAddress.getByAddress(ipv6); - } catch (UnknownHostException e) { - throw new ApplicationException(e); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - NetworkAddress that = (NetworkAddress) o; - - return port == that.port && Arrays.equals(ipv6, that.ipv6); - } - - @Override - public int hashCode() { - int result = ipv6 != null ? Arrays.hashCode(ipv6) : 0; - result = 31 * result + port; - return result; - } - - @Override - public String toString() { - return "[" + toInetAddress() + "]:" + port; - } - - @Override - public void write(OutputStream stream) throws IOException { - write(stream, false); - } - - public void write(OutputStream out, boolean light) throws IOException { - if (!light) { - Encode.int64(time, out); - Encode.int32(stream, out); - } - Encode.int64(services, out); - out.write(ipv6); - Encode.int16(port, out); - } - - @Override - public void write(ByteBuffer buffer) { - write(buffer, false); - } - - public void write(ByteBuffer buffer, boolean light) { - if (!light) { - Encode.int64(time, buffer); - Encode.int32(stream, buffer); - } - Encode.int64(services, buffer); - buffer.put(ipv6); - Encode.int16(port, buffer); - } - - public static final class Builder { - private long time; - private long stream; - private long services = 1; - private byte[] ipv6; - private int port; - - public Builder time(final long time) { - this.time = time; - return this; - } - - public Builder stream(final long stream) { - this.stream = stream; - return this; - } - - public Builder services(final long services) { - this.services = services; - return this; - } - - public Builder ip(InetAddress inetAddress) { - byte[] addr = inetAddress.getAddress(); - if (addr.length == 16) { - this.ipv6 = addr; - } else if (addr.length == 4) { - this.ipv6 = new byte[16]; - this.ipv6[10] = (byte) 0xff; - this.ipv6[11] = (byte) 0xff; - System.arraycopy(addr, 0, this.ipv6, 12, 4); - } else { - throw new IllegalArgumentException("Weird address " + inetAddress); - } - return this; - } - - public Builder ipv6(byte[] ipv6) { - this.ipv6 = ipv6; - return this; - } - - public Builder ipv6(int p00, int p01, int p02, int p03, - int p04, int p05, int p06, int p07, - int p08, int p09, int p10, int p11, - int p12, int p13, int p14, int p15) { - this.ipv6 = new byte[]{ - (byte) p00, (byte) p01, (byte) p02, (byte) p03, - (byte) p04, (byte) p05, (byte) p06, (byte) p07, - (byte) p08, (byte) p09, (byte) p10, (byte) p11, - (byte) p12, (byte) p13, (byte) p14, (byte) p15 - }; - return this; - } - - public Builder ipv4(int p00, int p01, int p02, int p03) { - this.ipv6 = new byte[]{ - (byte) 0, (byte) 0, (byte) 0x00, (byte) 0x00, - (byte) 0, (byte) 0, (byte) 0x00, (byte) 0x00, - (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff, - (byte) p00, (byte) p01, (byte) p02, (byte) p03 - }; - return this; - } - - public Builder port(final int port) { - this.port = port; - return this; - } - - public Builder address(SocketAddress address) { - if (address instanceof InetSocketAddress) { - InetSocketAddress inetAddress = (InetSocketAddress) address; - ip(inetAddress.getAddress()); - port(inetAddress.getPort()); - } else { - throw new IllegalArgumentException("Unknown type of address: " + address.getClass()); - } - return this; - } - - public NetworkAddress build() { - if (time == 0) { - time = UnixTime.now(); - } - return new NetworkAddress(this); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java deleted file mode 100644 index 7621ca5..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java +++ /dev/null @@ -1,198 +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.entity.valueobject; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.utils.Bytes; -import ch.dissem.bitmessage.utils.Decode; -import ch.dissem.bitmessage.utils.Encode; - -import java.io.*; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying - * {@link Pubkey} object. - */ -public class PrivateKey implements Streamable { - private static final long serialVersionUID = 8562555470709110558L; - - public static final int PRIVATE_KEY_SIZE = 32; - - private final byte[] privateSigningKey; - private final byte[] privateEncryptionKey; - - private final Pubkey pubkey; - - public PrivateKey(boolean shorter, long stream, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { - byte[] privSK; - byte[] pubSK; - byte[] privEK; - byte[] pubEK; - byte[] ripe; - do { - privSK = cryptography().randomBytes(PRIVATE_KEY_SIZE); - privEK = cryptography().randomBytes(PRIVATE_KEY_SIZE); - pubSK = cryptography().createPublicKey(privSK); - pubEK = cryptography().createPublicKey(privEK); - ripe = Pubkey.getRipe(pubSK, pubEK); - } while (ripe[0] != 0 || (shorter && ripe[1] != 0)); - this.privateSigningKey = privSK; - this.privateEncryptionKey = privEK; - this.pubkey = cryptography().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, - nonceTrialsPerByte, extraBytes, features); - } - - public PrivateKey(byte[] privateSigningKey, byte[] privateEncryptionKey, Pubkey pubkey) { - this.privateSigningKey = privateSigningKey; - this.privateEncryptionKey = privateEncryptionKey; - this.pubkey = pubkey; - } - - public PrivateKey(BitmessageAddress address, String passphrase) { - this(address.getVersion(), address.getStream(), passphrase); - } - - public PrivateKey(long version, long stream, String passphrase) { - this(new Builder(version, stream, false).seed(passphrase).generate()); - } - - private PrivateKey(Builder builder) { - this.privateSigningKey = builder.privSK; - this.privateEncryptionKey = builder.privEK; - this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK, builder.pubEK, - InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES); - } - - private static class Builder { - final long version; - final long stream; - final boolean shorter; - - byte[] seed; - long nextNonce; - - byte[] privSK, privEK; - byte[] pubSK, pubEK; - - private Builder(long version, long stream, boolean shorter) { - this.version = version; - this.stream = stream; - this.shorter = shorter; - } - - Builder seed(String passphrase) { - try { - seed = passphrase.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - return this; - } - - Builder generate() { - long signingKeyNonce = nextNonce; - long encryptionKeyNonce = nextNonce + 1; - byte[] ripe; - do { - privEK = Bytes.truncate(cryptography().sha512(seed, Encode.varInt(encryptionKeyNonce)), 32); - privSK = Bytes.truncate(cryptography().sha512(seed, Encode.varInt(signingKeyNonce)), 32); - pubSK = cryptography().createPublicKey(privSK); - pubEK = cryptography().createPublicKey(privEK); - ripe = cryptography().ripemd160(cryptography().sha512(pubSK, pubEK)); - - signingKeyNonce += 2; - encryptionKeyNonce += 2; - } while (ripe[0] != 0 || (shorter && ripe[1] != 0)); - nextNonce = signingKeyNonce; - return this; - } - } - - public static List<PrivateKey> deterministic(String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) { - List<PrivateKey> result = new ArrayList<>(numberOfAddresses); - Builder builder = new Builder(version, stream, shorter).seed(passphrase); - for (int i = 0; i < numberOfAddresses; i++) { - builder.generate(); - result.add(new PrivateKey(builder)); - } - return result; - } - - public static PrivateKey read(InputStream is) throws IOException { - int version = (int) Decode.varInt(is); - long stream = Decode.varInt(is); - int len = (int) Decode.varInt(is); - Pubkey pubkey = Factory.readPubkey(version, stream, is, len, false); - len = (int) Decode.varInt(is); - byte[] signingKey = Decode.bytes(is, len); - len = (int) Decode.varInt(is); - byte[] encryptionKey = Decode.bytes(is, len); - return new PrivateKey(signingKey, encryptionKey, pubkey); - } - - public byte[] getPrivateSigningKey() { - return privateSigningKey; - } - - public byte[] getPrivateEncryptionKey() { - return privateEncryptionKey; - } - - public Pubkey getPubkey() { - return pubkey; - } - - @Override - public void write(OutputStream out) throws IOException { - Encode.varInt(pubkey.getVersion(), out); - Encode.varInt(pubkey.getStream(), out); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - pubkey.writeUnencrypted(baos); - Encode.varInt(baos.size(), out); - out.write(baos.toByteArray()); - Encode.varInt(privateSigningKey.length, out); - out.write(privateSigningKey); - Encode.varInt(privateEncryptionKey.length, out); - out.write(privateEncryptionKey); - } - - - @Override - public void write(ByteBuffer buffer) { - Encode.varInt(pubkey.getVersion(), buffer); - Encode.varInt(pubkey.getStream(), buffer); - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - pubkey.writeUnencrypted(baos); - Encode.varBytes(baos.toByteArray(), buffer); - } catch (IOException e) { - throw new ApplicationException(e); - } - Encode.varBytes(privateSigningKey, buffer); - Encode.varBytes(privateEncryptionKey, buffer); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java deleted file mode 100644 index 42afc4f..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java +++ /dev/null @@ -1,100 +0,0 @@ -package ch.dissem.bitmessage.entity.valueobject.extended; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.Objects; - -/** - * A "file" attachment as used by extended encoding type messages. Could either be an attachment, - * or used inline to be used by a HTML message, for example. - */ -public class Attachment implements Serializable { - private static final long serialVersionUID = 7319139427666943189L; - - private String name; - private byte[] data; - private String type; - private Disposition disposition; - - public String getName() { - return name; - } - - public byte[] getData() { - return data; - } - - public String getType() { - return type; - } - - public Disposition getDisposition() { - return disposition; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Attachment that = (Attachment) o; - return Objects.equals(name, that.name) && - Arrays.equals(data, that.data) && - Objects.equals(type, that.type) && - disposition == that.disposition; - } - - @Override - public int hashCode() { - return Objects.hash(name, data, type, disposition); - } - - public enum Disposition { - inline, attachment - } - - public static final class Builder { - private String name; - private byte[] data; - private String type; - private Disposition disposition; - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder data(byte[] data) { - this.data = data; - return this; - } - - public Builder type(String type) { - this.type = type; - return this; - } - - public Builder inline() { - this.disposition = Disposition.inline; - return this; - } - - public Builder attachment() { - this.disposition = Disposition.attachment; - return this; - } - - public Builder disposition(Disposition disposition) { - this.disposition = disposition; - return this; - } - - public Attachment build() { - Attachment attachment = new Attachment(); - attachment.type = this.type; - attachment.disposition = this.disposition; - attachment.data = this.data; - attachment.name = this.name; - return attachment; - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java deleted file mode 100644 index aea4368..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java +++ /dev/null @@ -1,219 +0,0 @@ -package ch.dissem.bitmessage.entity.valueobject.extended; - -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.msgpack.types.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.URLConnection; -import java.nio.file.Files; -import java.util.*; - -import static ch.dissem.bitmessage.entity.valueobject.extended.Attachment.Disposition.attachment; -import static ch.dissem.bitmessage.utils.Strings.str; -import static ch.dissem.msgpack.types.Utils.mp; - -/** - * Extended encoding type 'message'. Properties 'parents' and 'files' not yet supported by PyBitmessage, so they might not work - * properly with future PyBitmessage implementations. - */ -public class Message implements ExtendedEncoding.ExtendedType { - private static final long serialVersionUID = -2724977231484285467L; - private static final Logger LOG = LoggerFactory.getLogger(Message.class); - - public static final String TYPE = "message"; - - private String subject; - private String body; - private List<InventoryVector> parents; - private List<Attachment> files; - - private Message(Builder builder) { - subject = builder.subject; - body = builder.body; - parents = Collections.unmodifiableList(builder.parents); - files = Collections.unmodifiableList(builder.files); - } - - @Override - public String getType() { - return TYPE; - } - - public String getSubject() { - return subject; - } - - public String getBody() { - return body; - } - - public List<InventoryVector> getParents() { - return parents; - } - - public List<Attachment> getFiles() { - return files; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Message message = (Message) o; - return Objects.equals(subject, message.subject) && - Objects.equals(body, message.body) && - Objects.equals(parents, message.parents) && - Objects.equals(files, message.files); - } - - @Override - public int hashCode() { - return Objects.hash(subject, body, parents, files); - } - - @Override - public MPMap<MPString, MPType<?>> pack() throws IOException { - MPMap<MPString, MPType<?>> result = new MPMap<>(); - result.put(mp(""), mp(TYPE)); - result.put(mp("subject"), mp(subject)); - result.put(mp("body"), mp(body)); - - if (!files.isEmpty()) { - MPArray<MPMap<MPString, MPType<?>>> items = new MPArray<>(); - result.put(mp("files"), items); - for (Attachment file : files) { - MPMap<MPString, MPType<?>> item = new MPMap<>(); - item.put(mp("name"), mp(file.getName())); - item.put(mp("data"), mp(file.getData())); - item.put(mp("type"), mp(file.getType())); - item.put(mp("disposition"), mp(file.getDisposition().name())); - items.add(item); - } - } - if (!parents.isEmpty()) { - MPArray<MPBinary> items = new MPArray<>(); - result.put(mp("parents"), items); - for (InventoryVector parent : parents) { - items.add(mp(parent.getHash())); - } - } - return result; - } - - public static class Builder { - private String subject; - private String body; - private List<InventoryVector> parents = new LinkedList<>(); - private List<Attachment> files = new LinkedList<>(); - - public Builder subject(String subject) { - this.subject = subject; - return this; - } - - public Builder body(String body) { - this.body = body; - return this; - } - - public Builder addParent(Plaintext parent) { - if (parent != null) { - InventoryVector iv = parent.getInventoryVector(); - if (iv == null) { - LOG.debug("Ignored parent without IV"); - } else { - parents.add(iv); - } - } - return this; - } - - public Builder addParent(InventoryVector iv) { - if (iv != null) { - parents.add(iv); - } - return this; - } - - public Builder addFile(File file, Attachment.Disposition disposition) { - if (file != null) { - try { - files.add(new Attachment.Builder() - .name(file.getName()) - .disposition(disposition) - .type(URLConnection.guessContentTypeFromStream(new FileInputStream(file))) - .data(Files.readAllBytes(file.toPath())) - .build()); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - } - } - return this; - } - - public Builder addFile(Attachment file) { - if (file != null) { - files.add(file); - } - return this; - } - - public ExtendedEncoding build() { - return new ExtendedEncoding(new Message(this)); - } - } - - public static class Unpacker implements ExtendedEncoding.Unpacker<Message> { - @Override - public String getType() { - return TYPE; - } - - @Override - public Message unpack(MPMap<MPString, MPType<?>> map) { - Message.Builder builder = new Message.Builder(); - builder.subject(str(map.get(mp("subject")))); - builder.body(str(map.get(mp("body")))); - @SuppressWarnings("unchecked") - MPArray<MPBinary> parents = (MPArray<MPBinary>) map.get(mp("parents")); - if (parents != null) { - for (MPBinary parent : parents) { - builder.addParent(InventoryVector.fromHash(parent.getValue())); - } - } - @SuppressWarnings("unchecked") - MPArray<MPMap<MPString, MPType<?>>> files = (MPArray<MPMap<MPString, MPType<?>>>) map.get(mp("files")); - if (files != null) { - for (MPMap<MPString, MPType<?>> item : files) { - Attachment.Builder b = new Attachment.Builder(); - b.name(str(item.get(mp("name")))); - b.data(bin(item.get(mp("data")))); - b.type(str(item.get(mp("type")))); - String disposition = str(item.get(mp("disposition"))); - if ("inline".equals(disposition)) { - b.inline(); - } else if ("attachment".equals(disposition)) { - b.attachment(); - } - builder.addFile(b.build()); - } - } - - return new Message(builder); - } - - private byte[] bin(MPType data) { - if (data instanceof MPBinary) { - return ((MPBinary) data).getValue(); - } else { - return null; - } - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java deleted file mode 100644 index 1c13440..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java +++ /dev/null @@ -1,114 +0,0 @@ -package ch.dissem.bitmessage.entity.valueobject.extended; - -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.msgpack.types.*; - -import java.io.IOException; -import java.util.Objects; - -import static ch.dissem.bitmessage.utils.Strings.str; -import static ch.dissem.msgpack.types.Utils.mp; - -/** - * Extended encoding type 'vote'. Specification still outstanding, so this will need some work. - */ -public class Vote implements ExtendedEncoding.ExtendedType { - private static final long serialVersionUID = -8427038604209964837L; - - public static final String TYPE = "vote"; - - private InventoryVector msgId; - private String vote; - - private Vote(Builder builder) { - msgId = builder.msgId; - vote = builder.vote; - } - - @Override - public String getType() { - return TYPE; - } - - public InventoryVector getMsgId() { - return msgId; - } - - public String getVote() { - return vote; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Vote vote1 = (Vote) o; - return Objects.equals(msgId, vote1.msgId) && - Objects.equals(vote, vote1.vote); - } - - @Override - public int hashCode() { - return Objects.hash(msgId, vote); - } - - @Override - public MPMap<MPString, MPType<?>> pack() throws IOException { - MPMap<MPString, MPType<?>> result = new MPMap<>(); - result.put(mp(""), mp(TYPE)); - result.put(mp("msgId"), mp(msgId.getHash())); - result.put(mp("vote"), mp(vote)); - return result; - } - - public static class Builder { - private InventoryVector msgId; - private String vote; - - public ExtendedEncoding up(Plaintext message) { - msgId = message.getInventoryVector(); - vote = "1"; - return new ExtendedEncoding(new Vote(this)); - } - - public ExtendedEncoding down(Plaintext message) { - msgId = message.getInventoryVector(); - vote = "1"; - return new ExtendedEncoding(new Vote(this)); - } - - public Builder msgId(InventoryVector iv) { - this.msgId = iv; - return this; - } - - public Builder vote(String vote) { - this.vote = vote; - return this; - } - - public ExtendedEncoding build() { - return new ExtendedEncoding(new Vote(this)); - } - } - - public static class Unpacker implements ExtendedEncoding.Unpacker<Vote> { - @Override - public String getType() { - return TYPE; - } - - @Override - public Vote unpack(MPMap<MPString, MPType<?>> map) { - Vote.Builder builder = new Vote.Builder(); - MPType<?> msgId = map.get(mp("msgId")); - if (msgId instanceof MPBinary) { - builder.msgId(InventoryVector.fromHash(((MPBinary) msgId).getValue())); - } - builder.vote(str(map.get(mp("vote")))); - return new Vote(builder); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java b/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java deleted file mode 100644 index 94a5d84..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java +++ /dev/null @@ -1,30 +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.exception; - -import ch.dissem.bitmessage.utils.Strings; - -import java.io.IOException; -import java.util.Arrays; - -public class InsufficientProofOfWorkException extends IOException { - private static final long serialVersionUID = 9105580366564571318L; - - public InsufficientProofOfWorkException(byte[] target, byte[] hash) { - super("Insufficient proof of work: " + Strings.hex(target) + " required, " + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved."); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/BufferPool.java b/core/src/main/java/ch/dissem/bitmessage/factory/BufferPool.java deleted file mode 100644 index 65c1d34..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/factory/BufferPool.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.factory; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.util.Map; -import java.util.Stack; -import java.util.TreeMap; - -import static ch.dissem.bitmessage.ports.NetworkHandler.HEADER_SIZE; -import static ch.dissem.bitmessage.ports.NetworkHandler.MAX_PAYLOAD_SIZE; - -/** - * A pool for {@link ByteBuffer}s. As they may use up a lot of memory, - * they should be reused as efficiently as possible. - */ -class BufferPool { - private static final Logger LOG = LoggerFactory.getLogger(BufferPool.class); - - public static final BufferPool bufferPool = new BufferPool(); - - private final Map<Integer, Stack<ByteBuffer>> pools = new TreeMap<>(); - - private BufferPool() { - pools.put(HEADER_SIZE, new Stack<ByteBuffer>()); - pools.put(54, new Stack<ByteBuffer>()); - pools.put(1000, new Stack<ByteBuffer>()); - pools.put(60000, new Stack<ByteBuffer>()); - pools.put(MAX_PAYLOAD_SIZE, new Stack<ByteBuffer>()); - } - - public synchronized ByteBuffer allocate(int capacity) { - Integer targetSize = getTargetSize(capacity); - Stack<ByteBuffer> pool = pools.get(targetSize); - if (pool.isEmpty()) { - LOG.trace("Creating new buffer of size " + targetSize); - return ByteBuffer.allocate(targetSize); - } else { - return pool.pop(); - } - } - - /** - * Returns a buffer that has the size of the Bitmessage network message header, 24 bytes. - * - * @return a buffer of size 24 - */ - public synchronized ByteBuffer allocateHeaderBuffer() { - Stack<ByteBuffer> pool = pools.get(HEADER_SIZE); - if (pool.isEmpty()) { - return ByteBuffer.allocate(HEADER_SIZE); - } else { - return pool.pop(); - } - } - - public synchronized void deallocate(ByteBuffer buffer) { - buffer.clear(); - Stack<ByteBuffer> pool = pools.get(buffer.capacity()); - if (pool == null) { - throw new IllegalArgumentException("Illegal buffer capacity " + buffer.capacity() + - " one of " + pools.keySet() + " expected."); - } else { - pool.push(buffer); - } - } - - private Integer getTargetSize(int capacity) { - for (Integer size : pools.keySet()) { - if (size >= capacity) return size; - } - throw new IllegalArgumentException("Requested capacity too large: " + - "requested=" + capacity + "; max=" + MAX_PAYLOAD_SIZE); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java deleted file mode 100644 index b94b5f9..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package ch.dissem.bitmessage.factory; - -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.extended.Message; -import ch.dissem.bitmessage.entity.valueobject.extended.Vote; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.msgpack.Reader; -import ch.dissem.msgpack.types.MPMap; -import ch.dissem.msgpack.types.MPString; -import ch.dissem.msgpack.types.MPType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.zip.InflaterInputStream; - -import static ch.dissem.bitmessage.utils.Strings.str; - -/** - * Factory that creates {@link ExtendedEncoding} objects from byte arrays. You can register your own types by adding a - * {@link ExtendedEncoding.Unpacker} using {@link #registerFactory(ExtendedEncoding.Unpacker)}. - */ -public class ExtendedEncodingFactory { - private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncodingFactory.class); - private static final ExtendedEncodingFactory INSTANCE = new ExtendedEncodingFactory(); - private static final MPString KEY_MESSAGE_TYPE = new MPString(""); - private Map<String, ExtendedEncoding.Unpacker<?>> factories = new HashMap<>(); - - private ExtendedEncodingFactory() { - registerFactory(new Message.Unpacker()); - registerFactory(new Vote.Unpacker()); - } - - public void registerFactory(ExtendedEncoding.Unpacker<?> factory) { - factories.put(factory.getType(), factory); - } - - - public ExtendedEncoding unzip(byte[] zippedData) { - try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { - Reader reader = Reader.getInstance(); - @SuppressWarnings("unchecked") - MPMap<MPString, MPType<?>> map = (MPMap<MPString, MPType<?>>) reader.read(unzipper); - MPType<?> messageType = map.get(KEY_MESSAGE_TYPE); - if (messageType == null) { - LOG.error("Missing message type"); - return null; - } - ExtendedEncoding.Unpacker<?> factory = factories.get(str(messageType)); - return new ExtendedEncoding(factory.unpack(map)); - } catch (ClassCastException | IOException e) { - throw new ApplicationException(e); - } - } - - public static ExtendedEncodingFactory getInstance() { - return INSTANCE; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java b/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java deleted file mode 100644 index 07b3161..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ /dev/null @@ -1,217 +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.factory; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.entity.ObjectMessage; -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.TTL; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.net.SocketException; -import java.net.SocketTimeoutException; - -import static ch.dissem.bitmessage.entity.payload.ObjectType.MSG; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * Creates {@link NetworkMessage} objects from {@link InputStream InputStreams} - */ -public class Factory { - private static final Logger LOG = LoggerFactory.getLogger(Factory.class); - - public static NetworkMessage getNetworkMessage(int version, InputStream stream) throws SocketTimeoutException { - try { - return V3MessageFactory.read(stream); - } catch (SocketTimeoutException | NodeException e) { - throw e; - } catch (SocketException e) { - throw new NodeException(e.getMessage(), e); - } catch (Exception e) { - LOG.error(e.getMessage(), e); - return null; - } - } - - public static ObjectMessage getObjectMessage(int version, InputStream stream, int length) { - try { - return V3MessageFactory.readObject(stream, length); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - return null; - } - } - - public static Pubkey createPubkey(long version, long stream, byte[] publicSigningKey, byte[] publicEncryptionKey, - long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { - return createPubkey(version, stream, publicSigningKey, publicEncryptionKey, nonceTrialsPerByte, extraBytes, - Pubkey.Feature.bitfield(features)); - } - - public static Pubkey createPubkey(long version, long stream, byte[] publicSigningKey, byte[] publicEncryptionKey, - long nonceTrialsPerByte, long extraBytes, int behaviourBitfield) { - if (publicSigningKey.length != 64 && publicSigningKey.length != 65) - throw new IllegalArgumentException("64 bytes signing key expected, but it was " - + publicSigningKey.length + " bytes long."); - if (publicEncryptionKey.length != 64 && publicEncryptionKey.length != 65) - throw new IllegalArgumentException("64 bytes encryption key expected, but it was " - + publicEncryptionKey.length + " bytes long."); - - switch ((int) version) { - case 2: - return new V2Pubkey.Builder() - .stream(stream) - .publicSigningKey(publicSigningKey) - .publicEncryptionKey(publicEncryptionKey) - .behaviorBitfield(behaviourBitfield) - .build(); - case 3: - return new V3Pubkey.Builder() - .stream(stream) - .publicSigningKey(publicSigningKey) - .publicEncryptionKey(publicEncryptionKey) - .behaviorBitfield(behaviourBitfield) - .nonceTrialsPerByte(nonceTrialsPerByte) - .extraBytes(extraBytes) - .build(); - case 4: - return new V4Pubkey( - new V3Pubkey.Builder() - .stream(stream) - .publicSigningKey(publicSigningKey) - .publicEncryptionKey(publicEncryptionKey) - .behaviorBitfield(behaviourBitfield) - .nonceTrialsPerByte(nonceTrialsPerByte) - .extraBytes(extraBytes) - .build() - ); - default: - throw new IllegalArgumentException("Unexpected pubkey version " + version); - } - } - - public static BitmessageAddress createIdentityFromPrivateKey(String address, - byte[] privateSigningKey, byte[] privateEncryptionKey, - long nonceTrialsPerByte, long extraBytes, - int behaviourBitfield) { - BitmessageAddress temp = new BitmessageAddress(address); - PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey, - createPubkey(temp.getVersion(), temp.getStream(), - cryptography().createPublicKey(privateSigningKey), - cryptography().createPublicKey(privateEncryptionKey), - nonceTrialsPerByte, extraBytes, behaviourBitfield)); - BitmessageAddress result = new BitmessageAddress(privateKey); - if (!result.getAddress().equals(address)) { - throw new IllegalArgumentException("Address not matching private key. Address: " + address - + "; Address derived from private key: " + result.getAddress()); - } - return result; - } - - 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 { - ObjectType type = ObjectType.fromNumber(objectType); - if (type != null) { - switch (type) { - case GET_PUBKEY: - return parseGetPubkey(version, streamNumber, stream, length); - case PUBKEY: - return parsePubkey(version, streamNumber, stream, length); - case MSG: - return parseMsg(version, streamNumber, stream, length); - case BROADCAST: - return parseBroadcast(version, streamNumber, stream, length); - default: - LOG.error("This should not happen, someone broke something in the code!"); - } - } - // fallback: just store the message - we don't really care what it is - LOG.trace("Unexpected object type: " + objectType); - return GenericPayload.read(version, streamNumber, stream, length); - } - - private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { - return GetPubkey.read(stream, streamNumber, length, version); - } - - public static Pubkey readPubkey(long version, long stream, InputStream is, int length, boolean encrypted) throws IOException { - switch ((int) version) { - case 2: - return V2Pubkey.read(is, stream); - case 3: - return V3Pubkey.read(is, stream); - case 4: - return V4Pubkey.read(is, stream, length, encrypted); - } - LOG.debug("Unexpected pubkey version " + version + ", handling as generic payload object"); - return null; - } - - private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { - Pubkey pubkey = readPubkey(version, streamNumber, stream, length, true); - return pubkey != null ? pubkey : GenericPayload.read(version, streamNumber, stream, length); - } - - private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException { - return Msg.read(stream, streamNumber, length); - } - - private static ObjectPayload parseBroadcast(long version, long streamNumber, InputStream stream, int length) throws IOException { - switch ((int) version) { - case 4: - return V4Broadcast.read(stream, streamNumber, length); - case 5: - return V5Broadcast.read(stream, streamNumber, length); - default: - LOG.debug("Encountered unknown broadcast version " + version); - return GenericPayload.read(version, streamNumber, stream, length); - } - } - - public static Broadcast getBroadcast(Plaintext plaintext) { - BitmessageAddress sendingAddress = plaintext.getFrom(); - if (sendingAddress.getVersion() < 4) { - return new V4Broadcast(sendingAddress, plaintext); - } else { - return new V5Broadcast(sendingAddress, plaintext); - } - } - - public static ObjectMessage createAck(Plaintext plaintext) { - if (plaintext == null || plaintext.getAckData() == null) - return null; - GenericPayload ack = new GenericPayload(3, plaintext.getFrom().getStream(), plaintext.getAckData()); - return new ObjectMessage.Builder().objectType(MSG).payload(ack).expiresTime(UnixTime.now(plaintext.getTTL())).build(); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java deleted file mode 100644 index 378f658..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ /dev/null @@ -1,236 +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.factory; - -import ch.dissem.bitmessage.entity.*; -import ch.dissem.bitmessage.entity.payload.GenericPayload; -import ch.dissem.bitmessage.entity.payload.ObjectPayload; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -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.Strings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * Creates protocol v3 network messages from {@link InputStream InputStreams} - */ -class V3MessageFactory { - private static Logger LOG = LoggerFactory.getLogger(V3MessageFactory.class); - - public static NetworkMessage read(InputStream in) throws IOException { - 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); - - if (testChecksum(checksum, payloadBytes)) { - MessagePayload payload = getPayload(command, new ByteArrayInputStream(payloadBytes), length); - if (payload != null) - return new NetworkMessage(payload); - else - return null; - } else { - throw new IOException("Checksum failed for message '" + command + "'"); - } - } - - static MessagePayload getPayload(String command, InputStream stream, int length) throws IOException { - switch (command) { - case "version": - return parseVersion(stream); - case "verack": - return new VerAck(); - case "addr": - return parseAddr(stream); - case "inv": - return parseInv(stream); - case "getdata": - 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); - long expiresTime = Decode.int64(in, counter); - long objectType = Decode.uint32(in, counter); - long version = Decode.varInt(in, counter); - long stream = Decode.varInt(in, counter); - - byte[] data = Decode.bytes(in, length - counter.length()); - ObjectPayload payload; - try { - ByteArrayInputStream dataStream = new ByteArrayInputStream(data); - payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length); - } catch (Exception e) { - if (LOG.isTraceEnabled()) { - LOG.trace("Could not parse object payload - using generic payload instead", e); - LOG.trace(Strings.hex(data).toString()); - } - payload = new GenericPayload(version, stream, data); - } - - return new ObjectMessage.Builder() - .nonce(nonce) - .expiresTime(expiresTime) - .objectType(objectType) - .stream(stream) - .payload(payload) - .build(); - } - - private static GetData parseGetData(InputStream stream) throws IOException { - long count = Decode.varInt(stream); - GetData.Builder builder = new GetData.Builder(); - for (int i = 0; i < count; i++) { - builder.addInventoryVector(parseInventoryVector(stream)); - } - return builder.build(); - } - - private static Inv parseInv(InputStream stream) throws IOException { - long count = Decode.varInt(stream); - Inv.Builder builder = new Inv.Builder(); - for (int i = 0; i < count; i++) { - builder.addInventoryVector(parseInventoryVector(stream)); - } - return builder.build(); - } - - private static Addr parseAddr(InputStream stream) throws IOException { - long count = Decode.varInt(stream); - Addr.Builder builder = new Addr.Builder(); - for (int i = 0; i < count; i++) { - builder.addAddress(parseAddress(stream, false)); - } - return builder.build(); - } - - private static Version parseVersion(InputStream stream) throws IOException { - int version = Decode.int32(stream); - long services = Decode.int64(stream); - long timestamp = Decode.int64(stream); - NetworkAddress addrRecv = parseAddress(stream, true); - NetworkAddress addrFrom = parseAddress(stream, true); - long nonce = Decode.int64(stream); - String userAgent = Decode.varString(stream); - long[] streamNumbers = Decode.varIntList(stream); - - return new Version.Builder() - .version(version) - .services(services) - .timestamp(timestamp) - .addrRecv(addrRecv).addrFrom(addrFrom) - .nonce(nonce) - .userAgent(userAgent) - .streams(streamNumbers).build(); - } - - private static InventoryVector parseInventoryVector(InputStream stream) throws IOException { - return InventoryVector.fromHash(Decode.bytes(stream, 32)); - } - - private static NetworkAddress parseAddress(InputStream stream, boolean light) throws IOException { - long time; - long streamNumber; - if (!light) { - time = Decode.int64(stream); - streamNumber = Decode.uint32(stream); // This isn't consistent, not sure if this is correct - } else { - time = 0; - streamNumber = 0; - } - long services = Decode.int64(stream); - byte[] ipv6 = Decode.bytes(stream, 16); - int port = Decode.uint16(stream); - return new NetworkAddress.Builder() - .time(time) - .stream(streamNumber) - .services(services) - .ipv6(ipv6) - .port(port) - .build(); - } - - private static boolean testChecksum(byte[] checksum, byte[] payload) { - byte[] payloadChecksum = cryptography().sha512(payload); - for (int i = 0; i < checksum.length; i++) { - if (checksum[i] != payloadChecksum[i]) { - return false; - } - } - return true; - } - - private static String getCommand(InputStream stream) throws IOException { - byte[] bytes = new byte[12]; - int end = bytes.length; - for (int i = 0; i < bytes.length; i++) { - bytes[i] = (byte) stream.read(); - if (end == bytes.length) { - if (bytes[i] == 0) end = i; - } else { - if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command"); - } - } - return new String(bytes, 0, end, "ASCII"); - } - - private static void findMagic(InputStream in) throws IOException { - int pos = 0; - for (int i = 0; i < 1620000; i++) { - byte b = (byte) in.read(); - if (b == MAGIC_BYTES[pos]) { - if (pos + 1 == MAGIC_BYTES.length) { - return; - } - } else if (pos > 0 && b == MAGIC_BYTES[0]) { - pos = 1; - } else { - pos = 0; - } - pos++; - } - throw new NodeException("Failed to find MAGIC bytes in stream"); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageReader.java b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageReader.java deleted file mode 100644 index 89677cd..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageReader.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.factory; - -import ch.dissem.bitmessage.entity.MessagePayload; -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.exception.NodeException; -import ch.dissem.bitmessage.utils.Decode; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; -import static ch.dissem.bitmessage.factory.BufferPool.bufferPool; -import static ch.dissem.bitmessage.ports.NetworkHandler.MAX_PAYLOAD_SIZE; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * Similar to the {@link V3MessageFactory}, but used for NIO buffers which may or may not contain a whole message. - */ -public class V3MessageReader { - private ByteBuffer headerBuffer; - private ByteBuffer dataBuffer; - - private ReaderState state = ReaderState.MAGIC; - private String command; - private int length; - private byte[] checksum; - - private List<NetworkMessage> messages = new LinkedList<>(); - - public ByteBuffer getActiveBuffer() { - if (state != null && state != ReaderState.DATA) { - if (headerBuffer == null) { - headerBuffer = bufferPool.allocateHeaderBuffer(); - } - } - return state == ReaderState.DATA ? dataBuffer : headerBuffer; - } - - public void update() { - if (state != ReaderState.DATA) { - getActiveBuffer(); - headerBuffer.flip(); - } - switch (state) { - case MAGIC: - if (!findMagicBytes(headerBuffer)) { - headerBuffer.compact(); - return; - } - state = ReaderState.HEADER; - case HEADER: - if (headerBuffer.remaining() < 20) { - headerBuffer.compact(); - headerBuffer.limit(20); - return; - } - command = getCommand(headerBuffer); - length = (int) Decode.uint32(headerBuffer); - if (length > MAX_PAYLOAD_SIZE) { - throw new NodeException("Payload of " + length + " bytes received, no more than " + - MAX_PAYLOAD_SIZE + " was expected."); - } - checksum = new byte[4]; - headerBuffer.get(checksum); - state = ReaderState.DATA; - bufferPool.deallocate(headerBuffer); - headerBuffer = null; - dataBuffer = bufferPool.allocate(length); - dataBuffer.clear(); - dataBuffer.limit(length); - case DATA: - if (dataBuffer.position() < length) { - return; - } else { - dataBuffer.flip(); - } - if (!testChecksum(dataBuffer)) { - state = ReaderState.MAGIC; - throw new NodeException("Checksum failed for message '" + command + "'"); - } - try { - MessagePayload payload = V3MessageFactory.getPayload( - command, - new ByteArrayInputStream(dataBuffer.array(), - dataBuffer.arrayOffset() + dataBuffer.position(), length), - length); - if (payload != null) { - messages.add(new NetworkMessage(payload)); - } - } catch (IOException e) { - throw new NodeException(e.getMessage()); - } finally { - state = ReaderState.MAGIC; - bufferPool.deallocate(dataBuffer); - dataBuffer = null; - dataBuffer = null; - } - } - } - - public List<NetworkMessage> getMessages() { - return messages; - } - - private boolean findMagicBytes(ByteBuffer buffer) { - int i = 0; - while (buffer.hasRemaining()) { - if (i == 0) { - buffer.mark(); - } - if (buffer.get() == MAGIC_BYTES[i]) { - i++; - if (i == MAGIC_BYTES.length) { - return true; - } - } else { - i = 0; - } - } - if (i > 0) { - buffer.reset(); - } - return false; - } - - private static String getCommand(ByteBuffer buffer) { - int start = buffer.position(); - int l = 0; - while (l < 12 && buffer.get() != 0) l++; - int i = l + 1; - while (i < 12) { - if (buffer.get() != 0) throw new NodeException("'\\0' padding expected for command"); - i++; - } - try { - return new String(buffer.array(), start, l, "ASCII"); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - } - - private boolean testChecksum(ByteBuffer buffer) { - byte[] payloadChecksum = cryptography().sha512(buffer.array(), - buffer.arrayOffset() + buffer.position(), length); - for (int i = 0; i < checksum.length; i++) { - if (checksum[i] != payloadChecksum[i]) { - return false; - } - } - return true; - } - - /** - * De-allocates all buffers. This method should be called iff the reader isn't used anymore, i.e. when its - * connection is severed. - */ - public void cleanup() { - state = null; - if (headerBuffer != null) { - bufferPool.deallocate(headerBuffer); - } - if (dataBuffer != null) { - bufferPool.deallocate(dataBuffer); - } - } - - private enum ReaderState {MAGIC, HEADER, DATA} -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java deleted file mode 100644 index 304c20d..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java +++ /dev/null @@ -1,214 +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.InternalContext; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.utils.Bytes; -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.*; - -import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; -import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; -import static ch.dissem.bitmessage.utils.Numbers.max; - -/** - * Implements everything that isn't directly dependent on either Spongy- or Bouncycastle. - */ -public abstract class AbstractCryptography implements Cryptography, InternalContext.ContextHolder { - protected 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); - - protected static final String ALGORITHM_ECDSA = "ECDSA"; - protected static final String ALGORITHM_ECDSA_SHA1 = "SHA1withECDSA"; - protected static final String ALGORITHM_EVP_SHA256 = "SHA256withECDSA"; - - protected final Provider provider; - private InternalContext context; - - protected AbstractCryptography(Provider provider) { - this.provider = provider; - } - - @Override - public void setContext(InternalContext context) { - this.context = context; - } - - public byte[] sha512(byte[] data, int offset, int length) { - MessageDigest mda = md("SHA-512"); - mda.update(data, offset, length); - return mda.digest(); - } - - 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, NETWORK_NONCE_TRIALS_PER_BYTE); - extraBytes = max(extraBytes, NETWORK_EXTRA_BYTES); - - 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); - } - } - - protected byte[] doSign(byte[] data, java.security.PrivateKey privKey) throws GeneralSecurityException { - // TODO: change this to ALGORITHM_EVP_SHA256 once it's generally used in the network - Signature sig = Signature.getInstance(ALGORITHM_ECDSA_SHA1, provider); - sig.initSign(privKey); - sig.update(data); - return sig.sign(); - } - - - protected boolean doCheckSignature(byte[] data, byte[] signature, PublicKey publicKey) throws GeneralSecurityException { - for (String algorithm : new String[]{ALGORITHM_ECDSA_SHA1, ALGORITHM_EVP_SHA256}) { - Signature sig = Signature.getInstance(algorithm, provider); - sig.initVerify(publicKey); - sig.update(data); - if (sig.verify(signature)) { - return true; - } - } - return false; - } - - @Override - public byte[] getInitialHash(ObjectMessage object) { - return sha512(object.getPayloadBytesWithoutNonce()); - } - - @Override - public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - if (nonceTrialsPerByte == 0) nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE; - if (extraBytes == 0) extraBytes = NETWORK_EXTRA_BYTES; - - BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now()); - BigInteger 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(TWO_POW_64.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 ApplicationException(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 ApplicationException(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/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java deleted file mode 100644 index 6184ff9..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.ports; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.utils.Strings; -import ch.dissem.bitmessage.utils.UnixTime; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static ch.dissem.bitmessage.utils.SqlStrings.join; - -public abstract class AbstractMessageRepository implements MessageRepository, InternalContext.ContextHolder { - protected InternalContext ctx; - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } - - /** - * @deprecated use {@link #saveContactIfNecessary(BitmessageAddress)} instead. - */ - @Deprecated - protected void safeSenderIfNecessary(Plaintext message) { - if (message.getId() == null) { - saveContactIfNecessary(message.getFrom()); - } - } - - protected void saveContactIfNecessary(BitmessageAddress contact) { - if (contact != null) { - BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(contact.getAddress()); - if (savedAddress == null) { - ctx.getAddressRepository().save(contact); - } else if (savedAddress.getPubkey() == null && contact.getPubkey() != null) { - savedAddress.setPubkey(contact.getPubkey()); - ctx.getAddressRepository().save(savedAddress); - } - if (savedAddress != null) { - contact.setAlias(savedAddress.getAlias()); - } - } - } - - @Override - public Plaintext getMessage(Object id) { - if (id instanceof Long) { - return single(find("id=" + id)); - } else { - throw new IllegalArgumentException("Long expected for ID"); - } - } - - @Override - public Plaintext getMessage(InventoryVector iv) { - return single(find("iv=X'" + Strings.hex(iv.getHash()) + "'")); - } - - @Override - public Plaintext getMessage(byte[] initialHash) { - return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'")); - } - - @Override - public Plaintext getMessageForAck(byte[] ackData) { - return single(find("ack_data=X'" + Strings.hex(ackData) + "' AND status='" + Plaintext.Status.SENT + "'")); - } - - @Override - public List<Plaintext> findMessages(Label label) { - if (label == null) { - return find("id NOT IN (SELECT message_id FROM Message_Label)"); - } else { - return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"); - } - } - - @Override - public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) { - return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'"); - } - - @Override - public List<Plaintext> findMessages(Plaintext.Status status) { - return find("status='" + status.name() + "'"); - } - - @Override - public List<Plaintext> findMessages(BitmessageAddress sender) { - return find("sender='" + sender.getAddress() + "'"); - } - - @Override - public List<Plaintext> findMessagesToResend() { - return find("status='" + Plaintext.Status.SENT.name() + "'" + - " AND next_try < " + UnixTime.now()); - } - - @Override - public List<Plaintext> findResponses(Plaintext parent) { - if (parent.getInventoryVector() == null) { - return Collections.emptyList(); - } - return find("iv IN (SELECT child FROM Message_Parent" - + " WHERE parent=X'" + Strings.hex(parent.getInventoryVector().getHash()) + "')"); - } - - @Override - public List<Plaintext> getConversation(UUID conversationId) { - return find("conversation=X'" + conversationId.toString().replace("-", "") + "'"); - } - - @Override - public List<Label> getLabels() { - return findLabels("1=1"); - } - - @Override - public List<Label> getLabels(Label.Type... types) { - return findLabels("type IN (" + join(types) + ")"); - } - - protected abstract List<Label> findLabels(String where); - - - protected <T> T single(Collection<T> collection) { - switch (collection.size()) { - case 0: - return null; - case 1: - return collection.iterator().next(); - default: - throw new ApplicationException("This shouldn't happen, found " + collection.size() + - " items, one or none was expected"); - } - } - - protected abstract List<Plaintext> find(String where); -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java b/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java deleted file mode 100644 index e4c2105..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.ports; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.Label; - -import static ch.dissem.bitmessage.entity.Plaintext.Status.*; - -public class DefaultLabeler implements Labeler, InternalContext.ContextHolder { - private InternalContext ctx; - - @Override - public void setLabels(Plaintext msg) { - msg.setStatus(RECEIVED); - if (msg.getType() == Plaintext.Type.BROADCAST) { - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)); - } else { - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD)); - } - } - - @Override - public void markAsDraft(Plaintext msg) { - msg.setStatus(DRAFT); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.DRAFT)); - } - - @Override - public void markAsSending(Plaintext msg) { - if (msg.getTo() != null && msg.getTo().getPubkey() == null) { - msg.setStatus(PUBKEY_REQUESTED); - } else { - msg.setStatus(DOING_PROOF_OF_WORK); - } - msg.removeLabel(Label.Type.DRAFT); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX)); - } - - @Override - public void markAsSent(Plaintext msg) { - msg.setStatus(SENT); - msg.removeLabel(Label.Type.OUTBOX); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); - } - - @Override - public void markAsAcknowledged(Plaintext msg) { - msg.setStatus(SENT_ACKNOWLEDGED); - } - - @Override - public void markAsRead(Plaintext msg) { - msg.removeLabel(Label.Type.UNREAD); - } - - @Override - public void markAsUnread(Plaintext msg) { - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.UNREAD)); - } - - @Override - public void delete(Plaintext msg) { - msg.getLabels().clear(); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.TRASH)); - } - - @Override - public void archive(Plaintext msg) { - msg.getLabels().clear(); - } - - @Override - public void setContext(InternalContext ctx) { - this.ctx = ctx; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java deleted file mode 100644 index ba34cfe..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java +++ /dev/null @@ -1,75 +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.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.Plaintext.Status; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; - -import java.util.Collection; -import java.util.List; -import java.util.UUID; - -public interface MessageRepository { - List<Label> getLabels(); - - List<Label> getLabels(Label.Type... types); - - int countUnread(Label label); - - Plaintext getMessage(Object id); - - Plaintext getMessage(InventoryVector iv); - - Plaintext getMessage(byte[] initialHash); - - Plaintext getMessageForAck(byte[] ackData); - - /** - * @param label to search for - * @return a distinct list of all conversations that have at least one message with the given label. - */ - List<UUID> findConversations(Label label); - - List<Plaintext> findMessages(Label label); - - List<Plaintext> findMessages(Status status); - - List<Plaintext> findMessages(Status status, BitmessageAddress recipient); - - List<Plaintext> findMessages(BitmessageAddress sender); - - List<Plaintext> findResponses(Plaintext parent); - - List<Plaintext> findMessagesToResend(); - - void save(Plaintext message); - - void remove(Plaintext message); - - /** - * Returns all messages with this conversation ID. The returned messages aren't sorted in any way, - * so you may prefer to use {@link ch.dissem.bitmessage.utils.ConversationService#getConversation(UUID)} - * instead. - * - * @param conversationId ID of the requested conversation - * @return all messages with the given conversation ID - */ - Collection<Plaintext> getConversation(UUID conversationId); -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java deleted file mode 100644 index fb45e50..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java +++ /dev/null @@ -1,128 +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.exception.ApplicationException; -import ch.dissem.bitmessage.utils.Bytes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.*; - -import static ch.dissem.bitmessage.utils.Bytes.inc; -import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool; - -/** - * A POW engine using all available CPU cores. - */ -public class MultiThreadedPOWEngine implements ProofOfWorkEngine { - private static final Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class); - private final ExecutorService waiterPool = Executors.newSingleThreadExecutor(pool("POW-waiter").daemon().build()); - private final ExecutorService workerPool = Executors.newCachedThreadPool(pool("POW-worker").daemon().build()); - - /** - * 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 void calculateNonce(final byte[] initialHash, final byte[] target, final Callback callback) { - waiterPool.execute(new Runnable() { - @Override - public void run() { - long startTime = System.currentTimeMillis(); - - int cores = Runtime.getRuntime().availableProcessors(); - if (cores > 255) cores = 255; - LOG.info("Doing POW using " + cores + " cores"); - List<Worker> workers = new ArrayList<>(cores); - for (int i = 0; i < cores; i++) { - Worker w = new Worker((byte) cores, i, initialHash, target); - workers.add(w); - } - List<Future<byte[]>> futures = new ArrayList<>(cores); - 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. - futures.add(workerPool.submit(w)); - } - try { - while (!Thread.interrupted()) { - for (Future<byte[]> future : futures) { - if (future.isDone()) { - callback.onNonceCalculated(initialHash, future.get()); - LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds"); - for (Future<byte[]> f : futures) { - f.cancel(true); - } - return; - } - } - Thread.sleep(100); - } - LOG.error("POW waiter thread interrupted - this should not happen!"); - } catch (ExecutionException e) { - LOG.error(e.getMessage(), e); - } catch (InterruptedException e) { - LOG.error("POW waiter thread interrupted - this should not happen!", e); - } - } - }); - } - - private class Worker implements Callable<byte[]> { - private final byte numberOfCores; - private final byte[] initialHash; - private final byte[] target; - private final MessageDigest mda; - private final byte[] nonce = new byte[8]; - - Worker(byte numberOfCores, int core, byte[] initialHash, byte[] target) { - this.numberOfCores = numberOfCores; - this.initialHash = initialHash; - this.target = target; - this.nonce[7] = (byte) core; - try { - mda = MessageDigest.getInstance("SHA-512"); - } catch (NoSuchAlgorithmException e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - } - - @Override - public byte[] call() throws Exception { - do { - inc(nonce, numberOfCores); - mda.update(nonce); - mda.update(initialHash); - if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) { - return nonce; - } - } while (!Thread.interrupted()); - return null; - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistryHelper.java b/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistryHelper.java deleted file mode 100644 index 63d70eb..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistryHelper.java +++ /dev/null @@ -1,54 +0,0 @@ -package ch.dissem.bitmessage.ports; - -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.exception.ApplicationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.util.*; - -/** - * Helper class to kick start node registries. - */ -public class NodeRegistryHelper { - private static final Logger LOG = LoggerFactory.getLogger(NodeRegistryHelper.class); - - public static Map<Long, Set<NetworkAddress>> loadStableNodes() { - try (InputStream in = NodeRegistryHelper.class.getClassLoader().getResourceAsStream("nodes.txt")) { - Scanner scanner = new Scanner(in); - long stream = 0; - Map<Long, Set<NetworkAddress>> result = new HashMap<>(); - Set<NetworkAddress> streamSet = null; - while (scanner.hasNext()) { - try { - String line = scanner.nextLine().trim(); - if (line.startsWith("[stream")) { - stream = Long.parseLong(line.substring(8, line.lastIndexOf(']'))); - streamSet = new HashSet<>(); - result.put(stream, streamSet); - } else if (streamSet != null && !line.isEmpty() && !line.startsWith("#")) { - int portIndex = line.lastIndexOf(':'); - InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex)); - int port = Integer.valueOf(line.substring(portIndex + 1)); - 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 : result.entrySet()) { - LOG.debug("Stream " + e.getKey() + ": loaded " + e.getValue().size() + " bootstrap nodes."); - } - } - return result; - } catch (IOException e) { - throw new ApplicationException(e); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java deleted file mode 100644 index b27a05d..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package ch.dissem.bitmessage.ports; - -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; - -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 putObject(Item item); - - void removeObject(byte[] initialHash); - - class Item { - public final ObjectMessage object; - public final long nonceTrialsPerByte; - public final long extraBytes; - - // Needed for ACK POW calculation - public final Long expirationTime; - public final Plaintext message; - - public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - this(object, nonceTrialsPerByte, extraBytes, 0, null); - } - - public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, long expirationTime, Plaintext message) { - this.object = object; - this.nonceTrialsPerByte = nonceTrialsPerByte; - this.extraBytes = extraBytes; - this.expirationTime = expirationTime; - this.message = message; - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java deleted file mode 100644 index a7d0d57..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java +++ /dev/null @@ -1,50 +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.exception.ApplicationException; -import ch.dissem.bitmessage.utils.Bytes; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import static ch.dissem.bitmessage.utils.Bytes.inc; - -/** - * You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one. - * <p> - * <strong>Warning:</strong> implementations probably depend on POW being asynchronous, that's - * another reason not to use this one. - * </p> - */ -public class SimplePOWEngine implements ProofOfWorkEngine { - @Override - public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { - try { - MessageDigest mda = MessageDigest.getInstance("SHA-512"); - byte[] nonce = new byte[8]; - do { - inc(nonce); - mda.update(nonce); - mda.update(initialHash); - } while (Bytes.lt(target, mda.digest(mda.digest()), 8)); - callback.onNonceCalculated(initialHash, nonce); - } catch (NoSuchAlgorithmException e) { - throw new ApplicationException(e); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java b/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java deleted file mode 100644 index a67e344..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2011 Google Inc. - * 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.exception.AddressFormatException; -import ch.dissem.bitmessage.exception.ApplicationException; - -import java.io.UnsupportedEncodingException; - -import static java.util.Arrays.copyOfRange; - -/** - * Base58 encoder and decoder. - * - * @author Christian Basler: I removed some dependencies to the BitcoinJ code so it can be used here more easily. - */ -public class Base58 { - private static final int[] INDEXES = new int[128]; - private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); - - static { - for (int i = 0; i < INDEXES.length; i++) { - INDEXES[i] = -1; - } - for (int i = 0; i < ALPHABET.length; i++) { - INDEXES[ALPHABET[i]] = i; - } - } - - /** - * Encodes the given bytes in base58. No checksum is appended. - * - * @param data to encode - * @return base58 encoded input - */ - public static String encode(byte[] data) { - if (data.length == 0) { - return ""; - } - final byte[] bytes = copyOfRange(data, 0, data.length); - // Count leading zeroes. - int zeroCount = 0; - while (zeroCount < bytes.length && bytes[zeroCount] == 0) { - ++zeroCount; - } - // The actual encoding. - byte[] temp = new byte[bytes.length * 2]; - int j = temp.length; - - int startAt = zeroCount; - while (startAt < bytes.length) { - byte mod = divmod58(bytes, startAt); - if (bytes[startAt] == 0) { - ++startAt; - } - temp[--j] = (byte) ALPHABET[mod]; - } - - // Strip extra '1' if there are some after decoding. - while (j < temp.length && temp[j] == ALPHABET[0]) { - ++j; - } - // Add as many leading '1' as there were leading zeros. - while (--zeroCount >= 0) { - temp[--j] = (byte) ALPHABET[0]; - } - - byte[] output = copyOfRange(temp, j, temp.length); - try { - return new String(output, "US-ASCII"); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); // Cannot happen. - } - } - - public static byte[] decode(String input) throws AddressFormatException { - if (input.length() == 0) { - return new byte[0]; - } - byte[] input58 = new byte[input.length()]; - // Transform the String to a base58 byte sequence - for (int i = 0; i < input.length(); ++i) { - char c = input.charAt(i); - - int digit58 = -1; - if (c < 128) { - digit58 = INDEXES[c]; - } - if (digit58 < 0) { - throw new AddressFormatException("Illegal character " + c + " at " + i); - } - - input58[i] = (byte) digit58; - } - // Count leading zeroes - int zeroCount = 0; - while (zeroCount < input58.length && input58[zeroCount] == 0) { - ++zeroCount; - } - // The encoding - byte[] temp = new byte[input.length()]; - int j = temp.length; - - int startAt = zeroCount; - while (startAt < input58.length) { - byte mod = divmod256(input58, startAt); - if (input58[startAt] == 0) { - ++startAt; - } - - temp[--j] = mod; - } - // Do no add extra leading zeroes, move j to first non null byte. - while (j < temp.length && temp[j] == 0) { - ++j; - } - return copyOfRange(temp, j - zeroCount, temp.length); - } - - // - // number -> number / 58, returns number % 58 - // - private static byte divmod58(byte[] number, int startAt) { - int remainder = 0; - for (int i = startAt; i < number.length; i++) { - int digit256 = (int) number[i] & 0xFF; - int temp = remainder * 256 + digit256; - - number[i] = (byte) (temp / 58); - - remainder = temp % 58; - } - - return (byte) remainder; - } - - // - // number -> number / 256, returns number % 256 - // - private static byte divmod256(byte[] number58, int startAt) { - int remainder = 0; - for (int i = startAt; i < number58.length; i++) { - int digit58 = (int) number58[i] & 0xFF; - int temp = remainder * 58 + digit58; - - number58[i] = (byte) (temp / 256); - - remainder = temp % 256; - } - - return (byte) remainder; - } -} \ No newline at end of file diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java index 986c288..15e174c 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,10 @@ package ch.dissem.bitmessage.utils; * This is one part due to the fact that Java doesn't support unsigned numbers, and another * part so we don't have to convert between byte arrays and numbers in time critical * situations. + * <p> + * Note: This class can't yet be ported to Kotlin, as with Kotlin byte + byte = int, which + * would be rather inefficient in our case. + * </p> */ public class Bytes { public static final byte BYTE_0x80 = (byte) 0x80; diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Collections.java b/core/src/main/java/ch/dissem/bitmessage/utils/Collections.java deleted file mode 100644 index 7eda028..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Collections.java +++ /dev/null @@ -1,71 +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 java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Random; - -public class Collections { - private final static Random RANDOM = new Random(); - - /** - * @param count the number of elements to return (if possible) - * @param collection the collection to take samples from - * @return a random subset of the given collection, or a copy of the collection if it's not larger than count. The - * result is by no means securely random, but should be random enough so not the same objects get selected over - * and over again. - */ - public static <T> List<T> selectRandom(int count, Collection<T> collection) { - ArrayList<T> result = new ArrayList<>(count); - if (collection.size() <= count) { - result.addAll(collection); - } else { - double collectionRest = collection.size(); - double resultRest = count; - int skipMax = (int) Math.ceil(collectionRest / resultRest); - int skip = RANDOM.nextInt(skipMax); - for (T item : collection) { - collectionRest--; - if (skip > 0) { - skip--; - } else { - result.add(item); - resultRest--; - if (resultRest == 0) { - break; - } - skipMax = (int) Math.ceil(collectionRest / resultRest); - skip = RANDOM.nextInt(skipMax); - } - } - } - return result; - } - - public static <T> T selectRandom(Collection<T> collection) { - int index = RANDOM.nextInt(collection.size()); - for (T item : collection) { - if (index == 0) { - return item; - } - index--; - } - throw new IllegalArgumentException("Empty collection? Size: " + collection.size()); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java b/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java deleted file mode 100644 index f261a08..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2017 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.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.ports.MessageRepository; - -import java.util.*; -import java.util.Collections; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.util.regex.Pattern.CASE_INSENSITIVE; - -/** - * Service that helps with conversations. - */ -public class ConversationService { - private final MessageRepository messageRepository; - - private final Pattern SUBJECT_PREFIX = Pattern.compile("^(re|fwd?):", CASE_INSENSITIVE); - - public ConversationService(MessageRepository messageRepository) { - this.messageRepository = messageRepository; - } - - /** - * Retrieve the whole conversation from one single message. If the message isn't part - * of a conversation, a singleton list containing the given message is returned. Otherwise - * it's the same as {@link #getConversation(UUID)} - * - * @param message - * @return a list of messages that belong to the same conversation. - */ - public List<Plaintext> getConversation(Plaintext message) { - if (message.getConversationId() == null) { - return Collections.singletonList(message); - } - return getConversation(message.getConversationId()); - } - - private LinkedList<Plaintext> sorted(Collection<Plaintext> collection) { - LinkedList<Plaintext> result = new LinkedList<>(collection); - Collections.sort(result, new Comparator<Plaintext>() { - @Override - public int compare(Plaintext o1, Plaintext o2) { - //noinspection NumberEquality - if both are null (if both are the same, it's a bonus) - if (o1.getReceived() == o2.getReceived()) { - return 0; - } - if (o1.getReceived() == null) { - return -1; - } - if (o2.getReceived() == null) { - return 1; - } - return -o1.getReceived().compareTo(o2.getReceived()); - } - }); - return result; - } - - public List<Plaintext> getConversation(UUID conversationId) { - LinkedList<Plaintext> messages = sorted(messageRepository.getConversation(conversationId)); - Map<InventoryVector, Plaintext> map = new HashMap<>(messages.size()); - for (Plaintext message : messages) { - if (message.getInventoryVector() != null) { - map.put(message.getInventoryVector(), message); - } - } - - LinkedList<Plaintext> result = new LinkedList<>(); - while (!messages.isEmpty()) { - Plaintext last = messages.poll(); - int pos = lastParentPosition(last, result); - result.add(pos, last); - addAncestors(last, result, messages, map); - } - return result; - } - - public String getSubject(List<Plaintext> conversation) { - if (conversation.isEmpty()) { - return null; - } - // TODO: this has room for improvement - String subject = conversation.get(0).getSubject(); - Matcher matcher = SUBJECT_PREFIX.matcher(subject); - if (matcher.find()) { - return subject.substring(matcher.end()).trim(); - } - return subject.trim(); - } - - private int lastParentPosition(Plaintext child, LinkedList<Plaintext> messages) { - Iterator<Plaintext> plaintextIterator = messages.descendingIterator(); - int i = 0; - while (plaintextIterator.hasNext()) { - Plaintext next = plaintextIterator.next(); - if (isParent(next, child)) { - break; - } - i++; - } - return messages.size() - i; - } - - private boolean isParent(Plaintext item, Plaintext child) { - for (InventoryVector parentId : child.getParents()) { - if (parentId.equals(item.getInventoryVector())) { - return true; - } - } - return false; - } - - private void addAncestors(Plaintext message, LinkedList<Plaintext> result, LinkedList<Plaintext> messages, Map<InventoryVector, Plaintext> map) { - for (InventoryVector parentKey : message.getParents()) { - Plaintext parent = map.remove(parentKey); - if (parent != null) { - messages.remove(parent); - result.addFirst(parent); - addAncestors(parent, result, messages, map); - } - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.java b/core/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.java deleted file mode 100644 index e2090b9..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/DebugUtils.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.utils; - -import ch.dissem.bitmessage.entity.ObjectMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Map; - -public class DebugUtils { - private final static Logger LOG = LoggerFactory.getLogger(DebugUtils.class); - - public static void saveToFile(ObjectMessage objectMessage) { - try { - File f = new File(System.getProperty("user.home") + "/jabit.error/" + objectMessage.getInventoryVector() + ".inv"); - f.createNewFile(); - objectMessage.write(new FileOutputStream(f)); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - } - - public static <K> void inc(Map<K, Integer> map, K key) { - if (map.containsKey(key)) { - map.put(key, map.get(key) + 1); - } else { - map.put(key, 1); - } - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java b/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java deleted file mode 100644 index 8d05e7e..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java +++ /dev/null @@ -1,152 +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 java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -import static ch.dissem.bitmessage.utils.AccessCounter.inc; - -/** - * This class handles decoding simple types from byte stream, according to - * https://bitmessage.org/wiki/Protocol_specification#Common_structures - */ -public class Decode { - public static byte[] shortVarBytes(InputStream in, AccessCounter counter) throws IOException { - int length = uint16(in, counter); - return bytes(in, length, counter); - } - - public static byte[] varBytes(InputStream in) throws IOException { - return varBytes(in, null); - } - - public static byte[] varBytes(InputStream in, AccessCounter counter) throws IOException { - int length = (int) varInt(in, counter); - return bytes(in, length, counter); - } - - public static byte[] bytes(InputStream in, int count) throws IOException { - return bytes(in, count, null); - } - - public static byte[] bytes(InputStream in, int count, AccessCounter counter) throws IOException { - byte[] result = new byte[count]; - int off = 0; - while (off < count) { - int read = in.read(result, off, count - off); - if (read < 0) { - throw new IOException("Unexpected end of stream, wanted to read " + count + " bytes but only got " + off); - } - off += read; - } - inc(counter, count); - return result; - } - - public static long[] varIntList(InputStream in) throws IOException { - int length = (int) varInt(in); - long[] result = new long[length]; - - for (int i = 0; i < length; i++) { - result[i] = varInt(in); - } - return result; - } - - public static long varInt(InputStream in) throws IOException { - return varInt(in, null); - } - - public static long varInt(InputStream in, AccessCounter counter) throws IOException { - int first = in.read(); - inc(counter); - switch (first) { - case 0xfd: - return uint16(in, counter); - case 0xfe: - return uint32(in, counter); - case 0xff: - return int64(in, counter); - default: - return first; - } - } - - public static int uint8(InputStream in) throws IOException { - return in.read(); - } - - public static int uint16(InputStream in) throws IOException { - return uint16(in, null); - } - - public static int uint16(InputStream in, AccessCounter counter) throws IOException { - inc(counter, 2); - return in.read() << 8 | in.read(); - } - - public static long uint32(InputStream in) throws IOException { - return uint32(in, null); - } - - public static long uint32(InputStream in, AccessCounter counter) throws IOException { - inc(counter, 4); - return in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read(); - } - - public static long uint32(ByteBuffer in) { - return u(in.get()) << 24 | u(in.get()) << 16 | u(in.get()) << 8 | u(in.get()); - } - - public static int int32(InputStream in) throws IOException { - return int32(in, null); - } - - public static int int32(InputStream in, AccessCounter counter) throws IOException { - inc(counter, 4); - return ByteBuffer.wrap(bytes(in, 4)).getInt(); - } - - public static long int64(InputStream in) throws IOException { - return int64(in, null); - } - - public static long int64(InputStream in, AccessCounter counter) throws IOException { - inc(counter, 8); - return ByteBuffer.wrap(bytes(in, 8)).getLong(); - } - - public static String varString(InputStream in) throws IOException { - return varString(in, null); - } - - public static String varString(InputStream in, AccessCounter counter) throws IOException { - int length = (int) varInt(in, counter); - // 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(in, length, counter), "utf-8"); - } - - /** - * Returns the given byte as if it were unsigned. - */ - private static int u(byte b) { - return b & 0xFF; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java b/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java deleted file mode 100644 index a60c027..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java +++ /dev/null @@ -1,207 +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.Streamable; -import ch.dissem.bitmessage.exception.ApplicationException; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.nio.Buffer; -import java.nio.ByteBuffer; - -import static ch.dissem.bitmessage.utils.AccessCounter.inc; - -/** - * This class handles encoding simple types from byte stream, according to - * https://bitmessage.org/wiki/Protocol_specification#Common_structures - */ -public class Encode { - public static void varIntList(long[] values, OutputStream stream) throws IOException { - varInt(values.length, stream); - for (long value : values) { - varInt(value, stream); - } - } - - public static void varIntList(long[] values, ByteBuffer buffer) { - varInt(values.length, buffer); - for (long value : values) { - varInt(value, buffer); - } - } - - public static void varInt(long value, OutputStream stream) throws IOException { - varInt(value, stream, null); - } - - public static void varInt(long value, ByteBuffer buffer) { - if (value < 0) { - // This is due to the fact that Java doesn't really support unsigned values. - // Please be aware that this might be an error due to a smaller negative value being cast to long. - // Normally, negative values shouldn't occur within the protocol, and longs large enough for being - // recognized as negatives aren't realistic. - buffer.put((byte) 0xff); - buffer.putLong(value); - } else if (value < 0xfd) { - buffer.put((byte) value); - } else if (value <= 0xffffL) { - buffer.put((byte) 0xfd); - buffer.putShort((short) value); - } else if (value <= 0xffffffffL) { - buffer.put((byte) 0xfe); - buffer.putInt((int) value); - } else { - buffer.put((byte) 0xff); - buffer.putLong(value); - } - } - - public static byte[] varInt(long value) { - ByteBuffer buffer = ByteBuffer.allocate(9); - varInt(value, buffer); - buffer.flip(); - return Bytes.truncate(buffer.array(), buffer.limit()); - } - - public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(9); - varInt(value, buffer); - buffer.flip(); - stream.write(buffer.array(), 0, buffer.limit()); - inc(counter, buffer.limit()); - } - - public static void int8(long value, OutputStream stream) throws IOException { - int8(value, stream, null); - } - - public static void int8(long value, OutputStream stream, AccessCounter counter) throws IOException { - stream.write((int) value); - inc(counter); - } - - public static void int16(long value, OutputStream stream) throws IOException { - int16(value, stream, null); - } - - public static void int16(long value, OutputStream stream, AccessCounter counter) throws IOException { - stream.write(ByteBuffer.allocate(2).putShort((short) value).array()); - inc(counter, 2); - } - - public static void int16(long value, ByteBuffer buffer) { - buffer.putShort((short) value); - } - - public static void int32(long value, OutputStream stream) throws IOException { - int32(value, stream, null); - } - - public static void int32(long value, OutputStream stream, AccessCounter counter) throws IOException { - stream.write(ByteBuffer.allocate(4).putInt((int) value).array()); - inc(counter, 4); - } - - public static void int32(long value, ByteBuffer buffer) { - buffer.putInt((int) value); - } - - public static void int64(long value, OutputStream stream) throws IOException { - int64(value, stream, null); - } - - public static void int64(long value, OutputStream stream, AccessCounter counter) throws IOException { - stream.write(ByteBuffer.allocate(8).putLong(value).array()); - inc(counter, 8); - } - - public static void int64(long value, ByteBuffer buffer) { - buffer.putLong(value); - } - - public static void varString(String value, OutputStream out) throws IOException { - byte[] bytes = value.getBytes("utf-8"); - // 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, out); - out.write(bytes); - } - - public static void varString(String value, ByteBuffer buffer) { - try { - byte[] bytes = value.getBytes("utf-8"); - // 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() - buffer.put(varInt(bytes.length)); - buffer.put(bytes); - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); - } - } - - public static void varBytes(byte[] data, OutputStream out) throws IOException { - varInt(data.length, out); - out.write(data); - } - - public static void varBytes(byte[] data, ByteBuffer buffer) { - varInt(data.length, buffer); - buffer.put(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. - */ - public static byte[] bytes(Streamable streamable) { - if (streamable == null) return null; - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - try { - streamable.write(stream); - } catch (IOException e) { - throw new ApplicationException(e); - } - return stream.toByteArray(); - } - - /** - * @param streamable the object to be serialized - * @param padding the result will be padded such that its length is a multiple of <em>padding</em> - * @return the bytes of the given {@link Streamable} object, 0-padded such that the final length is x*padding. - */ - public static byte[] bytes(Streamable streamable, int padding) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - try { - streamable.write(stream); - } catch (IOException e) { - throw new ApplicationException(e); - } - int offset = padding - stream.size() % padding; - int length = stream.size() + offset; - byte[] result = new byte[length]; - stream.write(result, offset, stream.size()); - return result; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java b/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java deleted file mode 100644 index 9d0c078..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java +++ /dev/null @@ -1,10 +0,0 @@ -package ch.dissem.bitmessage.utils; - -/** - * @author Christian Basler - */ -public class Numbers { - public static long max(long a, long b) { - return a > b ? a : b; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Property.java b/core/src/main/java/ch/dissem/bitmessage/utils/Property.java deleted file mode 100644 index b823eb5..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Property.java +++ /dev/null @@ -1,91 +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 java.util.Arrays; -import java.util.Objects; - -/** - * Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now - * used to contain different status information. It is by default displayed in some JSON inspired human readable - * 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; - private Object value; - private Property[] properties; - - public Property(String name, Object value, Property... properties) { - this.name = name; - this.value = value; - 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(""); - } - - private String toString(String indentation) { - StringBuilder result = new StringBuilder(); - result.append(indentation).append(name).append(": "); - if (value != null || properties.length == 0) { - result.append(value); - } - if (properties.length > 0) { - result.append("{\n"); - for (Property property : properties) { - result.append(property.toString(indentation + " ")).append('\n'); - } - result.append(indentation).append("}"); - } - return result.toString(); - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/SqlStrings.java b/core/src/main/java/ch/dissem/bitmessage/utils/SqlStrings.java deleted file mode 100644 index 82d06e3..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/SqlStrings.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.utils; - -import ch.dissem.bitmessage.entity.payload.ObjectType; - -import static ch.dissem.bitmessage.utils.Strings.hex; - -public class SqlStrings { - public static StringBuilder join(long... objects) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < objects.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(objects[i]); - } - return streamList; - } - - public static StringBuilder join(byte[]... objects) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < objects.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(hex(objects[i])); - } - return streamList; - } - - public static StringBuilder join(ObjectType... types) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < types.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(types[i].getNumber()); - } - return streamList; - } - - public static StringBuilder join(Enum... types) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < types.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append('\'').append(types[i].name()).append('\''); - } - return streamList; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java deleted file mode 100644 index c4fab9c..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java +++ /dev/null @@ -1,44 +0,0 @@ -package ch.dissem.bitmessage.utils; - -import static ch.dissem.bitmessage.utils.UnixTime.DAY; - -/** - * Stores times to live in seconds for different object types. Usually this shouldn't be messed with, but for tests - * it might be a good idea to reduce it to a minimum, and on mobile clients you might want to optimize it as well. - * - * @author Christian Basler - */ -public class TTL { - private static long msg = 2 * DAY; - private static long getpubkey = 2 * DAY; - private static long pubkey = 28 * DAY; - - public static long msg() { - return msg; - } - - public static void msg(long msg) { - TTL.msg = validate(msg); - } - - public static long getpubkey() { - return getpubkey; - } - - public static void getpubkey(long getpubkey) { - TTL.getpubkey = validate(getpubkey); - } - - public static long pubkey() { - return pubkey; - } - - public static void pubkey(long pubkey) { - TTL.pubkey = validate(pubkey); - } - - private static long validate(long ttl) { - if (ttl < 0 || ttl > 28 * DAY) throw new IllegalArgumentException("TTL must be between 0 seconds and 28 days"); - return ttl; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.java b/core/src/main/java/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.java deleted file mode 100644 index 36cf5b9..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.utils; - -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -public class ThreadFactoryBuilder { - private final String namePrefix; - private int prio = Thread.NORM_PRIORITY; - private boolean daemon = false; - - private ThreadFactoryBuilder(String pool) { - this.namePrefix = pool + "-thread-"; - } - - - public static ThreadFactoryBuilder pool(String name) { - return new ThreadFactoryBuilder(name); - } - - public ThreadFactoryBuilder lowPrio() { - prio = Thread.MIN_PRIORITY; - return this; - } - - public ThreadFactoryBuilder daemon() { - daemon = true; - return this; - } - - public ThreadFactory build() { - SecurityManager s = System.getSecurityManager(); - final ThreadGroup group = (s != null) ? s.getThreadGroup() : - Thread.currentThread().getThreadGroup(); - - return new ThreadFactory() { - private final AtomicInteger threadNumber = new AtomicInteger(1); - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(group, r, - namePrefix + threadNumber.getAndIncrement(), - 0); - t.setPriority(prio); - t.setDaemon(daemon); - return t; - } - }; - } -} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java b/core/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java deleted file mode 100644 index 0d0d991..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/utils/UnixTime.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.utils; - -/** - * A simple utility class that simplifies using the second based time used in Bitmessage. - */ -public class UnixTime { - /** - * Length of a minute in seconds, intended for use with {@link #now(long)}. - */ - public static final int MINUTE = 60; - /** - * Length of an hour in seconds, intended for use with {@link #now(long)}. - */ - public static final long HOUR = 60 * MINUTE; - /** - * Length of a day in seconds, intended for use with {@link #now(long)}. - */ - public static final long DAY = 24 * HOUR; - - /** - * @return the time in second based Unix time ({@link System#currentTimeMillis()}/1000) - */ - public static long now() { - return System.currentTimeMillis() / 1000; - } - - /** - * Same as {@link #now()} + shiftSeconds, but might be more readable. - * - * @param shiftSeconds number of seconds from now we're interested in - * @return the Unix time in shiftSeconds seconds / shiftSeconds seconds ago - */ - public static long now(long shiftSeconds) { - return (System.currentTimeMillis() / 1000) + shiftSeconds; - } -} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/BitmessageContext.kt b/core/src/main/kotlin/ch/dissem/bitmessage/BitmessageContext.kt new file mode 100644 index 0000000..2f09636 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/BitmessageContext.kt @@ -0,0 +1,509 @@ +/* + * 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.InternalContext.Companion.NETWORK_EXTRA_BYTES +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.CustomMessage +import ch.dissem.bitmessage.entity.MessagePayload +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Status.DRAFT +import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.Broadcast +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature +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.UnixTime.HOUR +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import org.slf4j.LoggerFactory +import java.net.InetAddress +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import kotlin.properties.Delegates + +/** + * + * 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) + */ +class BitmessageContext( + cryptography: Cryptography, + inventory: Inventory, + nodeRegistry: NodeRegistry, + networkHandler: NetworkHandler, + addressRepository: AddressRepository, + messageRepository: MessageRepository, + proofOfWorkRepository: ProofOfWorkRepository, + proofOfWorkEngine: ProofOfWorkEngine = MultiThreadedPOWEngine(), + customCommandHandler: CustomCommandHandler = object : CustomCommandHandler { + override fun handle(request: CustomMessage): MessagePayload? { + BitmessageContext.LOG.debug("Received custom request, but no custom command handler configured.") + return null + } + }, + listener: Listener, + labeler: Labeler = DefaultLabeler(), + userAgent: String? = null, + port: Int = 8444, + connectionTTL: Long = 30 * MINUTE, + connectionLimit: Int = 150, + sendPubkeyOnIdentityCreation: Boolean = true, + doMissingProofOfWorkDelayInSeconds: Int = 30 +) { + + private constructor(builder: BitmessageContext.Builder) : this( + builder.cryptography, + builder.inventory, + builder.nodeRegistry, + builder.networkHandler, + builder.addressRepo, + builder.messageRepo, + builder.proofOfWorkRepository, + builder.proofOfWorkEngine ?: MultiThreadedPOWEngine(), + builder.customCommandHandler ?: object : CustomCommandHandler { + override fun handle(request: CustomMessage): MessagePayload? { + BitmessageContext.LOG.debug("Received custom request, but no custom command handler configured.") + return null + } + }, + builder.listener, + builder.labeler ?: DefaultLabeler(), + builder.userAgent, + builder.port, + builder.connectionTTL, + builder.connectionLimit, + builder.sendPubkeyOnIdentityCreation, + builder.doMissingProofOfWorkDelay + ) + + private val sendPubkeyOnIdentityCreation: Boolean + + /** + * The [InternalContext] - normally you wouldn't need it, + * unless you are doing something crazy with the protocol. + */ + val internals: InternalContext + @JvmName("internals") get + + val labeler: Labeler + @JvmName("labeler") get + + val addresses: AddressRepository + @JvmName("addresses") get + + val messages: MessageRepository + @JvmName("messages") get + + fun createIdentity(shorter: Boolean, vararg features: Feature): BitmessageAddress { + val identity = BitmessageAddress(PrivateKey( + shorter, + internals.streams[0], + NETWORK_NONCE_TRIALS_PER_BYTE, + NETWORK_EXTRA_BYTES, + *features + )) + internals.addressRepository.save(identity) + if (sendPubkeyOnIdentityCreation) { + internals.sendPubkey(identity, identity.stream) + } + return identity + } + + fun joinChan(passphrase: String, address: String): BitmessageAddress { + val chan = BitmessageAddress.chan(address, passphrase) + chan.alias = passphrase + internals.addressRepository.save(chan) + return chan + } + + fun createChan(passphrase: String): BitmessageAddress { + // FIXME: hardcoded stream number + val chan = BitmessageAddress.chan(1, passphrase) + internals.addressRepository.save(chan) + return chan + } + + fun createDeterministicAddresses( + passphrase: String, numberOfAddresses: Int, version: Long, stream: Long, shorter: Boolean): List<BitmessageAddress> { + val result = BitmessageAddress.deterministic( + passphrase, numberOfAddresses, version, stream, shorter) + for (i in result.indices) { + val address = result[i] + address.alias = "deterministic (" + (i + 1) + ")" + internals.addressRepository.save(address) + } + return result + } + + fun broadcast(from: BitmessageAddress, subject: String, message: String) { + send(Plaintext( + type = BROADCAST, + from = from, + subject = subject, + body = message, + status = DRAFT + )) + } + + fun send(from: BitmessageAddress, to: BitmessageAddress, subject: String, message: String) { + if (from.privateKey == null) { + throw IllegalArgumentException("'From' must be an identity, i.e. have a private key.") + } + send(Plaintext( + type = MSG, + from = from, + to = to, + subject = subject, + body = message + )) + } + + fun send(msg: Plaintext) { + if (msg.from.privateKey == null) { + throw IllegalArgumentException("'From' must be an identity, i.e. have a private key.") + } + labeler.markAsSending(msg) + val to = msg.to + if (to != null) { + if (to.pubkey == null) { + LOG.info("Public key is missing from recipient. Requesting.") + internals.requestPubkey(to) + } + if (to.pubkey == null) { + internals.messageRepository.save(msg) + } + } + if (to == null || to.pubkey != null) { + LOG.info("Sending message.") + internals.messageRepository.save(msg) + if (msg.type == MSG) { + internals.send(msg) + } else { + internals.send( + msg.from, + to, + Factory.getBroadcast(msg), + msg.ttl + ) + } + } + } + + fun startup() { + internals.networkHandler.start() + } + + fun shutdown() { + internals.networkHandler.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 + */ + fun synchronize(host: InetAddress, port: Int, timeoutInSeconds: Long, wait: Boolean) { + val future = internals.networkHandler.synchronize(host, port, timeoutInSeconds) + if (wait) { + try { + future.get() + } catch (e: InterruptedException) { + LOG.info("Thread was interrupted. Trying to shut down synchronization and returning.") + future.cancel(true) + } catch (e: CancellationException) { + LOG.debug(e.message, e) + } catch (e: ExecutionException) { + LOG.debug(e.message, 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 [CustomMessage]. + + * @param server the node's address + * * + * @param port the node's port + * * + * @param request the request + * * + * @return the response + */ + fun send(server: InetAddress, port: Int, request: CustomMessage): CustomMessage { + return internals.networkHandler.send(server, port, request) + } + + /** + * Removes expired objects from the inventory. You should call this method regularly, + * e.g. daily and on each shutdown. + */ + fun cleanup() { + internals.inventory.cleanup() + } + + /** + * Sends messages again whose time to live expired without being acknowledged. (And whose + * recipient is expected to send acknowledgements. + * + * + * You should call this method regularly, but be aware of the following: + * + * * As messages might be sent, POW will be done. It is therefore not advised to + * call it on shutdown. + * * It shouldn't be called right after startup, as it's possible the missing + * acknowledgement was sent while the client was offline. + * * Other than that, the call isn't expensive as long as there is no message + * to send, so it might be a good idea to just call it every few minutes. + * + */ + fun resendUnacknowledgedMessages() { + internals.resendUnacknowledged() + } + + val isRunning: Boolean + get() = internals.networkHandler.isRunning + + fun addContact(contact: BitmessageAddress) { + internals.addressRepository.save(contact) + if (contact.pubkey == null) { + // If it already existed, the saved contact might have the public key + if (internals.addressRepository.getAddress(contact.address)!!.pubkey == null) { + internals.requestPubkey(contact) + } + } + } + + fun addSubscribtion(address: BitmessageAddress) { + address.isSubscribed = true + internals.addressRepository.save(address) + tryToFindBroadcastsForAddress(address) + } + + private fun tryToFindBroadcastsForAddress(address: BitmessageAddress) { + for (objectMessage in internals.inventory.getObjects(address.stream, Broadcast.getVersion(address), ObjectType.BROADCAST)) { + try { + val broadcast = objectMessage.payload as Broadcast + broadcast.decrypt(address) + // This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with + // other subscriptions and the interface stays as simple as possible. + internals.networkListener.receive(objectMessage) + } catch (ignore: DecryptionFailedException) { + } catch (e: Exception) { + LOG.debug(e.message, e) + } + } + } + + fun status(): Property { + return Property("status", + Property("user agent", internals.userAgent), + internals.networkHandler.getNetworkStatus(), + Property("unacknowledged", internals.messageRepository.findMessagesToResend().size) + ) + } + + interface Listener { + fun receive(plaintext: Plaintext) + + /** + * A message listener that needs a [BitmessageContext], i.e. for implementing some sort of chat bot. + */ + interface WithContext : Listener { + fun setContext(ctx: BitmessageContext) + } + } + + class Builder { + internal var port = 8444 + internal var inventory by Delegates.notNull<Inventory>() + internal var nodeRegistry by Delegates.notNull<NodeRegistry>() + internal var networkHandler by Delegates.notNull<NetworkHandler>() + internal var addressRepo by Delegates.notNull<AddressRepository>() + internal var messageRepo by Delegates.notNull<MessageRepository>() + internal var proofOfWorkRepository by Delegates.notNull<ProofOfWorkRepository>() + internal var proofOfWorkEngine: ProofOfWorkEngine? = null + internal var cryptography by Delegates.notNull<Cryptography>() + internal var customCommandHandler: CustomCommandHandler? = null + internal var labeler: Labeler? = null + internal var userAgent: String? = null + internal var listener by Delegates.notNull<Listener>() + internal var connectionLimit = 150 + internal var connectionTTL = 30 * MINUTE + internal var sendPubkeyOnIdentityCreation = true + internal var doMissingProofOfWorkDelay = 30 + + fun port(port: Int): Builder { + this.port = port + return this + } + + fun inventory(inventory: Inventory): Builder { + this.inventory = inventory + return this + } + + fun nodeRegistry(nodeRegistry: NodeRegistry): Builder { + this.nodeRegistry = nodeRegistry + return this + } + + fun networkHandler(networkHandler: NetworkHandler): Builder { + this.networkHandler = networkHandler + return this + } + + fun addressRepo(addressRepo: AddressRepository): Builder { + this.addressRepo = addressRepo + return this + } + + fun messageRepo(messageRepo: MessageRepository): Builder { + this.messageRepo = messageRepo + return this + } + + fun powRepo(proofOfWorkRepository: ProofOfWorkRepository): Builder { + this.proofOfWorkRepository = proofOfWorkRepository + return this + } + + fun cryptography(cryptography: Cryptography): Builder { + this.cryptography = cryptography + return this + } + + fun customCommandHandler(handler: CustomCommandHandler): Builder { + this.customCommandHandler = handler + return this + } + + fun proofOfWorkEngine(proofOfWorkEngine: ProofOfWorkEngine): Builder { + this.proofOfWorkEngine = proofOfWorkEngine + return this + } + + fun labeler(labeler: Labeler): Builder { + this.labeler = labeler + return this + } + + fun listener(listener: Listener): Builder { + this.listener = listener + return this + } + + @JvmName("kotlinListener") + fun listener(listener: (Plaintext) -> Unit): Builder { + this.listener = object : Listener { + override fun receive(plaintext: Plaintext) { + listener.invoke(plaintext) + } + } + return this + } + + fun connectionLimit(connectionLimit: Int): Builder { + this.connectionLimit = connectionLimit + return this + } + + fun connectionTTL(hours: Int): Builder { + this.connectionTTL = hours * HOUR + return this + } + + fun doMissingProofOfWorkDelay(seconds: Int) { + this.doMissingProofOfWorkDelay = seconds + } + + /** + * By default a client will send the public key when an identity is being created. On weaker devices + * this behaviour might not be desirable. + */ + fun doNotSendPubkeyOnIdentityCreation(): Builder { + this.sendPubkeyOnIdentityCreation = false + return this + } + + fun build(): BitmessageContext { + return BitmessageContext(this) + } + } + + + init { + this.labeler = labeler + this.internals = InternalContext( + cryptography, + inventory, + nodeRegistry, + networkHandler, + addressRepository, + messageRepository, + proofOfWorkRepository, + proofOfWorkEngine, + customCommandHandler, + listener, + labeler, + userAgent?.let { "/$it/Jabit:$version/" } ?: "/Jabit:$version/", + port, + connectionTTL, + connectionLimit + ) + this.addresses = addressRepository + this.messages = messageRepository + this.sendPubkeyOnIdentityCreation = sendPubkeyOnIdentityCreation + (listener as? Listener.WithContext)?.setContext(this) + internals.proofOfWorkService.doMissingProofOfWork(doMissingProofOfWorkDelayInSeconds * 1000L) + } + + companion object { + @JvmField val CURRENT_VERSION = 3 + private val LOG = LoggerFactory.getLogger(BitmessageContext::class.java) + + val version: String by lazy { + BitmessageContext::class.java.getResource("/version")?.readText() ?: "local build" + } + @JvmStatic get + + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt b/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt new file mode 100644 index 0000000..ac30587 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2017 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.Plaintext.Status.PUBKEY_REQUESTED +import ch.dissem.bitmessage.entity.payload.* +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.exception.DecryptionFailedException +import ch.dissem.bitmessage.ports.Labeler +import ch.dissem.bitmessage.ports.NetworkHandler +import ch.dissem.bitmessage.utils.Strings.hex +import org.slf4j.LoggerFactory +import java.util.* + +open class DefaultMessageListener( + private val labeler: Labeler, + private val listener: BitmessageContext.Listener +) : NetworkHandler.MessageListener, InternalContext.ContextHolder { + private lateinit var ctx: InternalContext + + override fun setContext(context: InternalContext) { + ctx = context + } + + override fun receive(objectMessage: ObjectMessage) { + val payload = objectMessage.payload + + when (payload.type) { + ObjectType.GET_PUBKEY -> { + receive(objectMessage, payload as GetPubkey) + } + ObjectType.PUBKEY -> { + receive(payload as Pubkey) + } + ObjectType.MSG -> { + receive(objectMessage, payload as Msg) + } + ObjectType.BROADCAST -> { + receive(objectMessage, payload as Broadcast) + } + null -> { + if (payload is GenericPayload) { + receive(payload) + } + } + } + } + + protected fun receive(objectMessage: ObjectMessage, getPubkey: GetPubkey) { + val identity = ctx.addressRepository.findIdentity(getPubkey.ripeTag) + if (identity != null && identity.privateKey != null && !identity.isChan) { + LOG.info("Got pubkey request for identity " + identity) + // FIXME: only send pubkey if it wasn't sent in the last TTL.pubkey() days + ctx.sendPubkey(identity, objectMessage.stream) + } + } + + protected fun receive(pubkey: Pubkey) { + try { + if (pubkey is V4Pubkey) { + ctx.addressRepository.findContact(pubkey.tag)?.let { + if (it.pubkey == null) { + pubkey.decrypt(it.publicDecryptionKey) + updatePubkey(it, pubkey) + } + } + } else { + ctx.addressRepository.findContact(pubkey.ripe)?.let { + if (it.pubkey == null) { + updatePubkey(it, pubkey) + } + } + } + } catch (_: DecryptionFailedException) { + } + + } + + private fun updatePubkey(address: BitmessageAddress, pubkey: Pubkey) { + address.pubkey = pubkey + LOG.info("Got pubkey for contact " + address) + ctx.addressRepository.save(address) + val messages = ctx.messageRepository.findMessages(PUBKEY_REQUESTED, address) + LOG.info("Sending " + messages.size + " messages for contact " + address) + for (msg in messages) { + ctx.labeler.markAsSending(msg) + ctx.messageRepository.save(msg) + ctx.send(msg) + } + } + + protected fun receive(objectMessage: ObjectMessage, msg: Msg) { + for (identity in ctx.addressRepository.getIdentities()) { + try { + msg.decrypt(identity.privateKey!!.privateEncryptionKey) + val plaintext = msg.plaintext!! + plaintext.to = identity + if (!objectMessage.isSignatureValid(plaintext.from.pubkey!!)) { + LOG.warn("Msg with IV " + objectMessage.inventoryVector + " was successfully decrypted, but signature check failed. Ignoring.") + } else { + receive(objectMessage.inventoryVector, plaintext) + } + break + } catch (_: DecryptionFailedException) { + } + } + } + + protected fun receive(ack: GenericPayload) { + if (ack.data.size == Msg.ACK_LENGTH) { + ctx.messageRepository.getMessageForAck(ack.data)?.let { + ctx.labeler.markAsAcknowledged(it) + ctx.messageRepository.save(it) + } ?: LOG.debug("Message not found for ack ${hex(ack.data)}") + } + } + + protected fun receive(objectMessage: ObjectMessage, broadcast: Broadcast) { + val tag = (broadcast as? V5Broadcast)?.tag + ctx.addressRepository.getSubscriptions(broadcast.version) + .filter { tag == null || Arrays.equals(tag, it.tag) } + .forEach { + try { + broadcast.decrypt(it.publicDecryptionKey) + if (!objectMessage.isSignatureValid(broadcast.plaintext!!.from.pubkey!!)) { + LOG.warn("Broadcast with IV " + objectMessage.inventoryVector + " was successfully decrypted, but signature check failed. Ignoring.") + } else { + receive(objectMessage.inventoryVector, broadcast.plaintext!!) + } + } catch (_: DecryptionFailedException) { + } + } + } + + protected fun receive(iv: InventoryVector, msg: Plaintext) { + val contact = ctx.addressRepository.getAddress(msg.from.address) + if (contact != null && contact.pubkey == null) { + updatePubkey(contact, msg.from.pubkey!!) + } + + msg.inventoryVector = iv + labeler.setLabels(msg) + ctx.messageRepository.save(msg) + listener.receive(msg) + + if (msg.type == Plaintext.Type.MSG && msg.to!!.has(Pubkey.Feature.DOES_ACK)) { + msg.ackMessage?.let { + ctx.inventory.storeObject(it) + ctx.networkHandler.offer(it.inventoryVector) + } ?: LOG.debug("ack message expected") + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(DefaultMessageListener::class.java) + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/InternalContext.kt b/core/src/main/kotlin/ch/dissem/bitmessage/InternalContext.kt new file mode 100644 index 0000000..42ed782 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/InternalContext.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2017 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.Encrypted +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.payload.* +import ch.dissem.bitmessage.ports.* +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TTL +import ch.dissem.bitmessage.utils.UnixTime +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.Executors + +/** + * 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 + * get extended. + * + * + * On the other hand, if you need the BitmessageContext in a port implementation, the same thing might apply. + * + */ +class InternalContext( + val cryptography: Cryptography, + val inventory: ch.dissem.bitmessage.ports.Inventory, + val nodeRegistry: NodeRegistry, + val networkHandler: NetworkHandler, + val addressRepository: AddressRepository, + val messageRepository: ch.dissem.bitmessage.ports.MessageRepository, + val proofOfWorkRepository: ProofOfWorkRepository, + val proofOfWorkEngine: ProofOfWorkEngine, + val customCommandHandler: CustomCommandHandler, + listener: BitmessageContext.Listener, + val labeler: Labeler, + + val userAgent: String, + + val port: Int, + val connectionTTL: Long, + val connectionLimit: Int +) { + + private val threadPool = Executors.newCachedThreadPool() + + val proofOfWorkService: ProofOfWorkService = ProofOfWorkService() + val networkListener: NetworkHandler.MessageListener = DefaultMessageListener(labeler, listener) + val clientNonce: Long = cryptography.randomNonce() + private val _streams = TreeSet<Long>() + val streams: LongArray + get() = _streams.toLongArray() + + init { + Singleton.initialize(cryptography) + + // TODO: streams of new identities and subscriptions should also be added. This works only after a restart. + addressRepository.getIdentities().mapTo(_streams) { it.stream } + addressRepository.getSubscriptions().mapTo(_streams) { it.stream } + if (_streams.isEmpty()) { + _streams.add(1L) + } + + init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, + proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, customCommandHandler, labeler, + networkListener) + } + + private fun init(vararg objects: Any) { + objects.filter { it is ContextHolder }.forEach { (it as ContextHolder).setContext(this) } + } + + fun send(plaintext: Plaintext) { + if (plaintext.ackMessage != null) { + val expires = UnixTime.now + plaintext.ttl + LOG.info("Expires at " + expires) + proofOfWorkService.doProofOfWorkWithAck(plaintext, expires) + } else { + send(plaintext.from, plaintext.to, Msg(plaintext), plaintext.ttl) + } + } + + fun send(from: BitmessageAddress, to: BitmessageAddress?, payload: ObjectPayload, + timeToLive: Long) { + val recipient = to ?: from + val expires = UnixTime.now + timeToLive + LOG.info("Expires at " + expires) + val objectMessage = ObjectMessage( + stream = recipient.stream, + expiresTime = expires, + payload = payload + ) + if (objectMessage.isSigned) { + objectMessage.sign( + from.privateKey ?: throw IllegalArgumentException("The given sending address is no identity") + ) + } + if (payload is Broadcast) { + payload.encrypt() + } else if (payload is Encrypted) { + objectMessage.encrypt( + recipient.pubkey ?: throw IllegalArgumentException("The public key for the recipient isn't available") + ) + } + proofOfWorkService.doProofOfWork(to, objectMessage) + } + + fun sendPubkey(identity: BitmessageAddress, targetStream: Long) { + val expires = UnixTime.now + TTL.pubkey + LOG.info("Expires at " + expires) + val payload = identity.pubkey ?: throw IllegalArgumentException("The given address is no identity") + val response = ObjectMessage( + expiresTime = expires, + stream = targetStream, + payload = payload + ) + response.sign( + identity.privateKey ?: throw IllegalArgumentException("The given address is no identity") + ) + response.encrypt(cryptography.createPublicKey(identity.publicDecryptionKey)) + // TODO: remember that the pubkey is just about to be sent, and on which stream! + proofOfWorkService.doProofOfWork(response) + } + + /** + * Be aware that if the pubkey already exists in the inventory, the metods will not request it and the callback + * for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB. + */ + fun requestPubkey(contact: BitmessageAddress) { + threadPool.execute { + val stored = addressRepository.getAddress(contact.address) + + tryToFindMatchingPubkey(contact) + if (contact.pubkey != null) { + if (stored != null) { + stored.pubkey = contact.pubkey + addressRepository.save(stored) + } else { + addressRepository.save(contact) + } + return@execute + } + + if (stored == null) { + addressRepository.save(contact) + } + + val expires = UnixTime.now + TTL.getpubkey + LOG.info("Expires at $expires") + val request = ObjectMessage( + stream = contact.stream, + expiresTime = expires, + payload = GetPubkey(contact) + ) + proofOfWorkService.doProofOfWork(request) + } + } + + private fun tryToFindMatchingPubkey(address: BitmessageAddress) { + addressRepository.getAddress(address.address)?.let { + address.alias = it.alias + address.isSubscribed = it.isSubscribed + } + for (objectMessage in inventory.getObjects(address.stream, address.version, ObjectType.PUBKEY)) { + try { + val pubkey = objectMessage.payload as Pubkey + if (address.version == 4L) { + val v4Pubkey = pubkey as V4Pubkey + if (Arrays.equals(address.tag, v4Pubkey.tag)) { + v4Pubkey.decrypt(address.publicDecryptionKey) + if (objectMessage.isSignatureValid(v4Pubkey)) { + address.pubkey = v4Pubkey + addressRepository.save(address) + break + } else { + LOG.info("Found pubkey for $address but signature is invalid") + } + } + } else { + if (Arrays.equals(pubkey.ripe, address.ripe)) { + address.pubkey = pubkey + addressRepository.save(address) + break + } + } + } catch (e: Exception) { + LOG.debug(e.message, e) + } + } + } + + fun resendUnacknowledged() { + val messages = messageRepository.findMessagesToResend() + for (message in messages) { + send(message) + messageRepository.save(message) + } + } + + interface ContextHolder { + fun setContext(context: InternalContext) + } + + companion object { + private val LOG = LoggerFactory.getLogger(InternalContext::class.java) + + @JvmField val NETWORK_NONCE_TRIALS_PER_BYTE: Long = 1000 + @JvmField val NETWORK_EXTRA_BYTES: Long = 1000 + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ProofOfWorkService.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ProofOfWorkService.kt new file mode 100644 index 0000000..5d177a6 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ProofOfWorkService.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2017 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.InternalContext.Companion.NETWORK_EXTRA_BYTES +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE +import ch.dissem.bitmessage.entity.* +import ch.dissem.bitmessage.entity.payload.Msg +import ch.dissem.bitmessage.ports.ProofOfWorkEngine +import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item +import org.slf4j.LoggerFactory +import java.util.* + +/** + * @author Christian Basler + */ +class ProofOfWorkService : ProofOfWorkEngine.Callback, InternalContext.ContextHolder { + + private lateinit var ctx: InternalContext + private val cryptography by lazy { ctx.cryptography } + private val powRepo by lazy { ctx.proofOfWorkRepository } + private val messageRepo by lazy { ctx.messageRepository } + + override fun setContext(context: InternalContext) { + ctx = context + } + + fun doMissingProofOfWork(delayInMilliseconds: Long) { + val items = powRepo.getItems() + if (items.isEmpty()) return + + // Wait for 30 seconds, to let the application start up before putting heavy load on the CPU + Timer().schedule(object : TimerTask() { + override fun run() { + LOG.info("Doing POW for " + items.size + " tasks.") + for (initialHash in items) { + val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash) + cryptography.doProofOfWork(objectMessage, nonceTrialsPerByte, extraBytes, + this@ProofOfWorkService) + } + } + }, delayInMilliseconds) + } + + fun doProofOfWork(objectMessage: ObjectMessage) { + doProofOfWork(null, objectMessage) + } + + fun doProofOfWork(recipient: BitmessageAddress?, objectMessage: ObjectMessage) { + val pubkey = recipient?.pubkey + + val nonceTrialsPerByte = pubkey?.nonceTrialsPerByte ?: NETWORK_NONCE_TRIALS_PER_BYTE + val extraBytes = pubkey?.extraBytes ?: NETWORK_EXTRA_BYTES + + powRepo.putObject(objectMessage, nonceTrialsPerByte, extraBytes) + if (objectMessage.payload is PlaintextHolder) { + objectMessage.payload.plaintext?.let { + it.initialHash = cryptography.getInitialHash(objectMessage) + messageRepo.save(it) + } ?: LOG.error("PlaintextHolder without Plaintext shouldn't make it to the POW") + } + cryptography.doProofOfWork(objectMessage, nonceTrialsPerByte, extraBytes, this) + } + + fun doProofOfWorkWithAck(plaintext: Plaintext, expirationTime: Long) { + val ack = plaintext.ackMessage!! + messageRepo.save(plaintext) + val item = Item(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, + expirationTime, plaintext) + powRepo.putObject(item) + cryptography.doProofOfWork(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, this) + } + + override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) { + val (objectMessage, _, _, expirationTime, message) = powRepo.getItem(initialHash) + if (message == null) { + objectMessage.nonce = nonce + messageRepo.getMessage(initialHash)?.let { + it.inventoryVector = objectMessage.inventoryVector + it.updateNextTry() + ctx.labeler.markAsSent(it) + messageRepo.save(it) + } + ctx.inventory.storeObject(objectMessage) + ctx.networkHandler.offer(objectMessage.inventoryVector) + } else { + message.ackMessage!!.nonce = nonce + val newObjectMessage = ObjectMessage.Builder() + .stream(message.stream) + .expiresTime(expirationTime!!) + .payload(Msg(message)) + .build() + if (newObjectMessage.isSigned) { + newObjectMessage.sign(message.from.privateKey!!) + } + if (newObjectMessage.payload is Encrypted) { + newObjectMessage.encrypt(message.to!!.pubkey!!) + } + doProofOfWork(message.to, newObjectMessage) + } + powRepo.removeObject(initialHash) + } + + companion object { + private val LOG = LoggerFactory.getLogger(ProofOfWorkService::class.java) + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/constants/constants.kt b/core/src/main/kotlin/ch/dissem/bitmessage/constants/constants.kt new file mode 100644 index 0000000..23d4c63 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/constants/constants.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017 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.constants + +/** + * Some network constants + */ +object Network { + @JvmField val NETWORK_MAGIC_NUMBER = 8 + @JvmField val HEADER_SIZE = 24 + @JvmField val MAX_PAYLOAD_SIZE = 1600003 + @JvmField val MAX_MESSAGE_SIZE = HEADER_SIZE + MAX_PAYLOAD_SIZE +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Addr.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Addr.kt new file mode 100644 index 0000000..056dd63 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Addr.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.utils.Encode +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * The 'addr' command holds a list of known active Bitmessage nodes. + */ +data class Addr constructor(val addresses: List<NetworkAddress>) : MessagePayload { + override val command: MessagePayload.Command = MessagePayload.Command.ADDR + + override fun write(out: OutputStream) { + Encode.varInt(addresses.size, out) + for (address in addresses) { + address.write(out) + } + } + + override fun write(buffer: ByteBuffer) { + Encode.varInt(addresses.size, buffer) + for (address in addresses) { + address.write(buffer) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/BitmessageAddress.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/BitmessageAddress.kt new file mode 100644 index 0000000..c0e978b --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/BitmessageAddress.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2017 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.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature +import ch.dissem.bitmessage.entity.payload.V4Pubkey +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.utils.AccessCounter +import ch.dissem.bitmessage.utils.Base58 +import ch.dissem.bitmessage.utils.Bytes +import ch.dissem.bitmessage.utils.Decode.bytes +import ch.dissem.bitmessage.utils.Decode.varInt +import ch.dissem.bitmessage.utils.Encode +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.Serializable +import java.util.* + +/** + * A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address + * holding private keys. + */ +class BitmessageAddress : Serializable { + + val version: Long + val stream: Long + val ripe: ByteArray + val tag: ByteArray? + /** + * The private key used to decrypt Pubkey objects (for v4 addresses) and broadcasts. It's easier to just create + * it regardless of address version. + */ + val publicDecryptionKey: ByteArray + + val address: String + + var privateKey: PrivateKey? = null + private set + var pubkey: Pubkey? = null + set(pubkey) { + if (pubkey != null) { + if (pubkey is V4Pubkey) { + if (!Arrays.equals(tag, pubkey.tag)) + throw IllegalArgumentException("Pubkey has incompatible tag") + } + if (!Arrays.equals(ripe, pubkey.ripe)) + throw IllegalArgumentException("Pubkey has incompatible ripe") + field = pubkey + } + } + + + var alias: String? = null + var isSubscribed: Boolean = false + var isChan: Boolean = false + + internal constructor(version: Long, stream: Long, ripe: ByteArray) { + this.version = version + this.stream = stream + this.ripe = ripe + + val os = ByteArrayOutputStream() + Encode.varInt(version, os) + Encode.varInt(stream, os) + if (version < 4) { + val checksum = cryptography().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 + val checksum = cryptography().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 + val offset = Bytes.numberOfLeadingZeros(ripe) + os.write(ripe, offset, ripe.size - offset) + val checksum = cryptography().doubleSha512(os.toByteArray()) + os.write(checksum, 0, 4) + this.address = "BM-" + Base58.encode(os.toByteArray()) + } + + constructor(publicKey: Pubkey) : this(publicKey.version, publicKey.stream, publicKey.ripe) { + this.pubkey = publicKey + } + + constructor(address: String, passphrase: String) : this(address) { + val key = PrivateKey(this, passphrase) + if (!Arrays.equals(ripe, key.pubkey.ripe)) { + throw IllegalArgumentException("Wrong address or passphrase") + } + this.privateKey = key + this.pubkey = key.pubkey + } + + constructor(privateKey: PrivateKey) : this(privateKey.pubkey) { + this.privateKey = privateKey + } + + constructor(address: String) { + this.address = address + val bytes = Base58.decode(address.substring(3)) + val `in` = ByteArrayInputStream(bytes) + val counter = AccessCounter() + this.version = varInt(`in`, counter) + this.stream = varInt(`in`, counter) + this.ripe = Bytes.expand(bytes(`in`, bytes.size - counter.length() - 4), 20) + + // test checksum + var checksum = cryptography().doubleSha512(bytes, bytes.size - 4) + val expectedChecksum = bytes(`in`, 4) + for (i in 0..3) { + if (expectedChecksum[i] != checksum[i]) + throw IllegalArgumentException("Checksum of address failed") + } + if (version < 4) { + checksum = cryptography().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe) + this.tag = null + this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32) + } else { + checksum = cryptography().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe) + this.tag = Arrays.copyOfRange(checksum, 32, 64) + this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32) + } + } + + override fun toString(): String { + return alias ?: address + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BitmessageAddress) return false + return version == other.version && + stream == other.stream && + Arrays.equals(ripe, other.ripe) + } + + override fun hashCode(): Int { + return Arrays.hashCode(ripe) + } + + fun has(feature: Feature?): Boolean { + return feature?.isActive(pubkey?.behaviorBitfield ?: 0) ?: false + } + + companion object { + @JvmStatic fun chan(address: String, passphrase: String): BitmessageAddress { + val result = BitmessageAddress(address, passphrase) + result.isChan = true + return result + } + + @JvmStatic fun chan(stream: Long, passphrase: String): BitmessageAddress { + val privateKey = PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase) + val result = BitmessageAddress(privateKey) + result.isChan = true + return result + } + + @JvmStatic fun deterministic(passphrase: String, numberOfAddresses: Int, + version: Long, stream: Long, shorter: Boolean): List<BitmessageAddress> { + val result = ArrayList<BitmessageAddress>(numberOfAddresses) + val privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter) + for (pk in privateKeys) { + result.add(BitmessageAddress(pk)) + } + return result + } + + @JvmStatic fun calculateTag(version: Long, stream: Long, ripe: ByteArray): ByteArray { + val out = ByteArrayOutputStream() + Encode.varInt(version, out) + Encode.varInt(stream, out) + out.write(ripe) + return Arrays.copyOfRange(cryptography().doubleSha512(out.toByteArray()), 32, 64) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/CustomMessage.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/CustomMessage.kt new file mode 100644 index 0000000..a303c94 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/CustomMessage.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity + +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.utils.AccessCounter +import ch.dissem.bitmessage.utils.Decode.bytes +import ch.dissem.bitmessage.utils.Decode.varString +import ch.dissem.bitmessage.utils.Encode +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * @author Christian Basler + */ +open class CustomMessage(val customCommand: String, private val data: ByteArray? = null) : MessagePayload { + + override val command: MessagePayload.Command = MessagePayload.Command.CUSTOM + + val isError: Boolean + + fun getData(): ByteArray { + if (data != null) { + return data + } else { + val out = ByteArrayOutputStream() + write(out) + return out.toByteArray() + } + } + + override fun write(out: OutputStream) { + if (data != null) { + Encode.varString(customCommand, out) + out.write(data) + } else { + throw ApplicationException("Tried to write custom message without data. " + + "Programmer: did you forget to override #write()?") + } + } + + override fun write(buffer: ByteBuffer) { + if (data != null) { + Encode.varString(customCommand, buffer) + buffer.put(data) + } else { + throw ApplicationException("Tried to write custom message without data. " + + "Programmer: did you forget to override #write()?") + } + } + + companion object { + val COMMAND_ERROR = "ERROR" + + fun read(`in`: InputStream, length: Int): CustomMessage { + val counter = AccessCounter() + return CustomMessage(varString(`in`, counter), bytes(`in`, length - counter.length())) + } + + fun error(message: String): CustomMessage { + return CustomMessage(COMMAND_ERROR, message.toByteArray(charset("UTF-8"))) + } + } + + init { + this.isError = COMMAND_ERROR == customCommand + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Encrypted.kt similarity index 63% rename from core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java rename to core/src/main/kotlin/ch/dissem/bitmessage/entity/Encrypted.kt index 8b15371..913fbc1 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Encrypted.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Encrypted.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,18 @@ * limitations under the License. */ -package ch.dissem.bitmessage.entity; +package ch.dissem.bitmessage.entity -import ch.dissem.bitmessage.exception.DecryptionFailedException; - -import java.io.IOException; +import ch.dissem.bitmessage.exception.DecryptionFailedException /** * Used for objects that have encrypted content */ -public interface Encrypted { - void encrypt(byte[] publicKey) throws IOException; +interface Encrypted { + fun encrypt(publicKey: ByteArray) - void decrypt(byte[] privateKey) throws IOException, DecryptionFailedException; + @Throws(DecryptionFailedException::class) + fun decrypt(privateKey: ByteArray) - boolean isDecrypted(); + val isDecrypted: Boolean } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/GetData.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/GetData.kt new file mode 100644 index 0000000..feebea1 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/GetData.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.utils.Encode +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * The 'getdata' command is used to request objects from a node. + */ +class GetData constructor(var inventory: List<InventoryVector>) : MessagePayload { + + override val command: MessagePayload.Command = MessagePayload.Command.GETDATA + + override fun write(out: OutputStream) { + Encode.varInt(inventory.size, out) + for (iv in inventory) { + iv.write(out) + } + } + + override fun write(buffer: ByteBuffer) { + Encode.varInt(inventory.size, buffer) + for (iv in inventory) { + iv.write(buffer) + } + } + + companion object { + @JvmField val MAX_INVENTORY_SIZE = 50000 + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Inv.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Inv.kt new file mode 100644 index 0000000..b0c5af9 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Inv.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.utils.Encode +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items. + */ +class Inv constructor(val inventory: List<InventoryVector>) : MessagePayload { + + override val command: MessagePayload.Command = MessagePayload.Command.INV + + override fun write(out: OutputStream) { + Encode.varInt(inventory.size, out) + for (iv in inventory) { + iv.write(out) + } + } + + override fun write(buffer: ByteBuffer) { + Encode.varInt(inventory.size, buffer) + for (iv in inventory) { + iv.write(buffer) + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java b/core/src/main/kotlin/ch/dissem/bitmessage/entity/MessagePayload.kt similarity index 80% rename from core/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java rename to core/src/main/kotlin/ch/dissem/bitmessage/entity/MessagePayload.kt index 994952b..b57fc16 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/MessagePayload.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,15 @@ * limitations under the License. */ -package ch.dissem.bitmessage.entity; +package ch.dissem.bitmessage.entity /** * A command can hold a network message payload */ -public interface MessagePayload extends Streamable { - Command getCommand(); +interface MessagePayload : Streamable { + val command: Command - enum Command { + enum class Command { VERSION, VERACK, ADDR, INV, GETDATA, OBJECT, CUSTOM } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/NetworkMessage.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/NetworkMessage.kt new file mode 100644 index 0000000..c1a251d --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/NetworkMessage.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2017 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.Encode +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * A network message is exchanged between two nodes. + */ +data class NetworkMessage( + /** + * The actual data, a message or an object. Not to be confused with objectPayload. + */ + val payload: MessagePayload +) : Streamable { + + /** + * First 4 bytes of sha512(payload) + */ + private fun getChecksum(bytes: ByteArray): ByteArray { + val d = cryptography().sha512(bytes) + return byteArrayOf(d[0], d[1], d[2], d[3]) + } + + override fun write(out: OutputStream) { + // magic + Encode.int32(MAGIC, out) + + // ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected) + val command = payload.command.name.toLowerCase() + out.write(command.toByteArray(charset("ASCII"))) + for (i in command.length..11) { + out.write(0x0) + } + + val payloadBytes = Encode.bytes(payload) + + // Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would + // ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are + // larger than this. + Encode.int32(payloadBytes.size, out) + + // checksum + out.write(getChecksum(payloadBytes)) + + // message payload + out.write(payloadBytes) + } + + /** + * A more efficient implementation of the write method, writing header data to the provided buffer and returning + * a new buffer containing the payload. + + * @param headerBuffer where the header data is written to (24 bytes) + * * + * @return a buffer containing the payload, ready to be read. + */ + fun writeHeaderAndGetPayloadBuffer(headerBuffer: ByteBuffer): ByteBuffer { + return ByteBuffer.wrap(writeHeader(headerBuffer)) + } + + /** + * For improved memory efficiency, you should use [.writeHeaderAndGetPayloadBuffer] + * and write the header buffer as well as the returned payload buffer into the channel. + + * @param buffer where everything gets written to. Needs to be large enough for the whole message + * * to be written. + */ + override fun write(buffer: ByteBuffer) { + val payloadBytes = writeHeader(buffer) + buffer.put(payloadBytes) + } + + private fun writeHeader(out: ByteBuffer): ByteArray { + // magic + Encode.int32(MAGIC, out) + + // ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected) + val command = payload.command.name.toLowerCase() + out.put(command.toByteArray(charset("ASCII"))) + + for (i in command.length..11) { + out.put(0.toByte()) + } + + val payloadBytes = Encode.bytes(payload) + + // Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would + // ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are + // larger than this. + Encode.int32(payloadBytes.size, out) + + // checksum + out.put(getChecksum(payloadBytes)) + + // message payload + return payloadBytes + } + + companion object { + /** + * Magic value indicating message origin network, and used to seek to next message when stream state is unknown + */ + val MAGIC = 0xE9BEB4D9.toInt() + val MAGIC_BYTES = ByteBuffer.allocate(4).putInt(MAGIC).array() + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/ObjectMessage.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/ObjectMessage.kt new file mode 100644 index 0000000..255ca48 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/ObjectMessage.kt @@ -0,0 +1,228 @@ +/* + * Copyright 2017 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.entity.payload.ObjectPayload +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.exception.DecryptionFailedException +import ch.dissem.bitmessage.utils.Bytes +import ch.dissem.bitmessage.utils.Encode +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +/** + * The 'object' command sends an object that is shared throughout the network. + */ +data class ObjectMessage( + var nonce: ByteArray? = null, + val expiresTime: Long, + val payload: ObjectPayload, + val type: Long, + /** + * The object's version + */ + val version: Long, + val stream: Long +) : MessagePayload { + + override val command: MessagePayload.Command = MessagePayload.Command.OBJECT + + constructor( + nonce: ByteArray? = null, + expiresTime: Long, + payload: ObjectPayload, + stream: Long + ) : this( + nonce, + expiresTime, + payload, + payload.type?.number ?: throw IllegalArgumentException("payload must have type defined"), + payload.version, + stream + ) + + val inventoryVector: InventoryVector + get() { + return InventoryVector(Bytes.truncate(cryptography().doubleSha512( + nonce ?: throw IllegalStateException("nonce must be set"), + payloadBytesWithoutNonce + ), 32)) + } + + private val isEncrypted: Boolean + get() = payload is Encrypted && !payload.isDecrypted + + val isSigned: Boolean + get() = payload.isSigned + + private val bytesToSign: ByteArray + get() { + try { + val out = ByteArrayOutputStream() + writeHeaderWithoutNonce(out) + payload.writeBytesToSign(out) + return out.toByteArray() + } catch (e: IOException) { + throw ApplicationException(e) + } + } + + fun sign(key: PrivateKey) { + if (payload.isSigned) { + payload.signature = cryptography().getSignature(bytesToSign, key) + } + } + + @Throws(DecryptionFailedException::class) + fun decrypt(key: PrivateKey) { + if (payload is Encrypted) { + payload.decrypt(key.privateEncryptionKey) + } + } + + @Throws(DecryptionFailedException::class) + fun decrypt(privateEncryptionKey: ByteArray) { + if (payload is Encrypted) { + payload.decrypt(privateEncryptionKey) + } + } + + fun encrypt(publicEncryptionKey: ByteArray) { + if (payload is Encrypted) { + payload.encrypt(publicEncryptionKey) + } + } + + fun encrypt(publicKey: Pubkey) { + try { + if (payload is Encrypted) { + payload.encrypt(publicKey.encryptionKey) + } + } catch (e: IOException) { + throw ApplicationException(e) + } + + } + + fun isSignatureValid(pubkey: Pubkey): Boolean { + if (isEncrypted) throw IllegalStateException("Payload must be decrypted first") + return cryptography().isSignatureValid(bytesToSign, payload.signature ?: return false, pubkey) + } + + override fun write(out: OutputStream) { + out.write(nonce ?: ByteArray(8)) + out.write(payloadBytesWithoutNonce) + } + + override fun write(buffer: ByteBuffer) { + buffer.put(nonce ?: ByteArray(8)) + buffer.put(payloadBytesWithoutNonce) + } + + private fun writeHeaderWithoutNonce(out: OutputStream) { + Encode.int64(expiresTime, out) + Encode.int32(type, out) + Encode.varInt(version, out) + Encode.varInt(stream, out) + } + + val payloadBytesWithoutNonce: ByteArray by lazy { + val out = ByteArrayOutputStream() + writeHeaderWithoutNonce(out) + payload.write(out) + out.toByteArray() + } + + class Builder { + private var nonce: ByteArray? = null + private var expiresTime: Long = 0 + private var objectType: Long? = null + private var streamNumber: Long = 0 + private var payload: ObjectPayload? = null + + fun nonce(nonce: ByteArray): Builder { + this.nonce = nonce + return this + } + + fun expiresTime(expiresTime: Long): Builder { + this.expiresTime = expiresTime + return this + } + + fun objectType(objectType: Long): Builder { + this.objectType = objectType + return this + } + + fun objectType(objectType: ObjectType): Builder { + this.objectType = objectType.number + return this + } + + fun stream(streamNumber: Long): Builder { + this.streamNumber = streamNumber + return this + } + + fun payload(payload: ObjectPayload): Builder { + this.payload = payload + if (this.objectType == null) + this.objectType = payload.type?.number + return this + } + + fun build(): ObjectMessage { + return ObjectMessage( + nonce = nonce, + expiresTime = expiresTime, + type = objectType!!, + version = payload!!.version, + stream = if (streamNumber > 0) streamNumber else payload!!.stream, + payload = payload!! + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ObjectMessage) return false + return expiresTime == other.expiresTime && + type == other.type && + version == other.version && + stream == other.stream && + payload == other.payload + } + + override fun hashCode(): Int { + var result = Arrays.hashCode(nonce) + result = 31 * result + (expiresTime xor expiresTime.ushr(32)).toInt() + result = 31 * result + (type xor type.ushr(32)).toInt() + result = 31 * result + (version xor version.ushr(32)).toInt() + result = 31 * result + (stream xor stream.ushr(32)).toInt() + result = 31 * result + (payload.hashCode()) + return result + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt new file mode 100644 index 0000000..9a8c90e --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt @@ -0,0 +1,753 @@ +/* + * Copyright 2017 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.entity.Plaintext.Encoding.* +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.Msg +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.factory.ExtendedEncodingFactory +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.* +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.* +import java.nio.ByteBuffer +import java.util.* +import java.util.Collections +import kotlin.collections.HashSet + +private fun message(encoding: Plaintext.Encoding, subject: String, body: String): ByteArray = when (encoding) { + SIMPLE -> "Subject:$subject\nBody:$body".toByteArray() + EXTENDED -> Message.Builder().subject(subject).body(body).build().zip() + TRIVIAL -> (subject + body).toByteArray() + IGNORE -> ByteArray(0) +} + +private fun ackData(type: Plaintext.Type, ackData: ByteArray?): ByteArray? { + if (ackData != null) { + return ackData + } else if (type == MSG) { + return cryptography().randomBytes(Msg.ACK_LENGTH) + } else { + return null + } +} + +/** + * A plaintext message before encryption or after decryption. + */ +class Plaintext private constructor( + val type: Type, + val from: BitmessageAddress, + to: BitmessageAddress?, + val encodingCode: Long, + val message: ByteArray, + val ackData: ByteArray?, + ackMessage: Lazy<ObjectMessage?> = lazy { Factory.createAck(from, ackData, ttl) }, + val conversationId: UUID = UUID.randomUUID(), + var inventoryVector: InventoryVector? = null, + var signature: ByteArray? = null, + sent: Long? = null, + val received: Long? = null, + var initialHash: ByteArray? = null, + ttl: Long = TTL.msg, + val labels: MutableSet<Label> = HashSet(), + status: Status +) : Streamable { + + var id: Any? = null + set(id) { + if (this.id != null) throw IllegalStateException("ID already set") + field = id + } + + var to: BitmessageAddress? = to + set(to) { + if (to == null) { + return + } + if (this.to != null) { + if (this.to!!.version != 0L) + throw IllegalStateException("Correct address already set") + if (!Arrays.equals(this.to!!.ripe, to.ripe)) { + throw IllegalArgumentException("RIPEs don't match") + } + } + field = to + } + + val stream: Long + get() = to?.stream ?: from.stream + + val extendedData: ExtendedEncoding? by lazy { + if (encodingCode == EXTENDED.code) { + ExtendedEncodingFactory.unzip(message) + } else { + null + } + } + + val ackMessage: ObjectMessage? by ackMessage + + var status: Status = status + set(status) { + if (status != Status.RECEIVED && sent == null && status != Status.DRAFT) { + sent = UnixTime.now + } + field = status + } + + val encoding: Encoding? by lazy { Encoding.fromCode(encodingCode) } + var sent: Long? = sent + private set + var retries: Int = 0 + private set + var nextTry: Long? = null + private set + val ttl: Long = ttl + @JvmName("getTTL") get + + constructor( + type: Type, + from: BitmessageAddress, + to: BitmessageAddress?, + encoding: Encoding, + message: ByteArray, + ackData: ByteArray? = null, + conversationId: UUID = UUID.randomUUID(), + inventoryVector: InventoryVector? = null, + signature: ByteArray? = null, + received: Long? = null, + initialHash: ByteArray? = null, + ttl: Long = TTL.msg, + labels: MutableSet<Label> = HashSet(), + status: Status + ) : this( + type = type, + from = from, + to = to, + encodingCode = encoding.code, + message = message, + ackData = ackData(type, ackData), + conversationId = conversationId, + inventoryVector = inventoryVector, + signature = signature, + received = received, + initialHash = initialHash, + ttl = ttl, + labels = labels, + status = status + ) + + constructor( + type: Type, + from: BitmessageAddress, + to: BitmessageAddress?, + encoding: Long, + message: ByteArray, + ackMessage: ByteArray?, + conversationId: UUID = UUID.randomUUID(), + inventoryVector: InventoryVector? = null, + signature: ByteArray? = null, + received: Long? = null, + initialHash: ByteArray? = null, + ttl: Long = TTL.msg, + labels: MutableSet<Label> = HashSet(), + status: Status + ) : this( + type = type, + from = from, + to = to, + encodingCode = encoding, + message = message, + ackData = null, + ackMessage = lazy { + if (ackMessage != null && ackMessage.isNotEmpty()) { + Factory.getObjectMessage( + 3, + ByteArrayInputStream(ackMessage), + ackMessage.size) + } else null + }, + conversationId = conversationId, + inventoryVector = inventoryVector, + signature = signature, + received = received, + initialHash = initialHash, + ttl = ttl, + labels = labels, + status = status + ) + + constructor( + type: Type, + from: BitmessageAddress, + to: BitmessageAddress? = null, + encoding: Encoding = SIMPLE, + subject: String, + body: String, + ackData: ByteArray? = null, + conversationId: UUID = UUID.randomUUID(), + ttl: Long = TTL.msg, + labels: MutableSet<Label> = HashSet(), + status: Status = Status.DRAFT + ) : this( + type = type, + from = from, + to = to, + encoding = encoding, + message = message(encoding, subject, body), + ackData = ackData(type, ackData), + conversationId = conversationId, + inventoryVector = null, + signature = null, + received = null, + initialHash = null, + ttl = ttl, + labels = labels, + status = status + ) + + constructor(builder: Builder) : this( + type = builder.type, + from = builder.from ?: throw IllegalStateException("sender identity not set"), + to = builder.to, + encodingCode = builder.encoding, + message = builder.message, + ackData = builder.ackData, + ackMessage = lazy { + val ackMsg = builder.ackMessage + if (ackMsg != null && ackMsg.isNotEmpty()) { + Factory.getObjectMessage( + 3, + ByteArrayInputStream(ackMsg), + ackMsg.size) + } else { + Factory.createAck(builder.from!!, builder.ackData, builder.ttl) + } + }, + conversationId = builder.conversation ?: UUID.randomUUID(), + inventoryVector = builder.inventoryVector, + signature = builder.signature, + sent = builder.sent, + received = builder.received, + initialHash = null, + ttl = builder.ttl, + labels = builder.labels, + status = builder.status ?: Status.RECEIVED + ) { + id = builder.id + } + + fun write(out: OutputStream, includeSignature: Boolean) { + Encode.varInt(from.version, out) + Encode.varInt(from.stream, out) + if (from.pubkey == null) { + Encode.int32(0, out) + val empty = ByteArray(64) + out.write(empty) + out.write(empty) + if (from.version >= 3) { + Encode.varInt(0, out) + Encode.varInt(0, out) + } + } else { + Encode.int32(from.pubkey!!.behaviorBitfield, out) + out.write(from.pubkey!!.signingKey, 1, 64) + out.write(from.pubkey!!.encryptionKey, 1, 64) + if (from.version >= 3) { + Encode.varInt(from.pubkey!!.nonceTrialsPerByte, out) + Encode.varInt(from.pubkey!!.extraBytes, out) + } + } + if (type == MSG) { + out.write(to!!.ripe) + } + Encode.varInt(encodingCode, out) + Encode.varInt(message.size, out) + out.write(message) + if (type == MSG) { + if (to?.has(Feature.DOES_ACK) ?: false) { + val ack = ByteArrayOutputStream() + ackMessage?.write(ack) + Encode.varBytes(ack.toByteArray(), out) + } else { + Encode.varInt(0, out) + } + } + if (includeSignature) { + if (signature == null) { + Encode.varInt(0, out) + } else { + Encode.varBytes(signature!!, out) + } + } + } + + fun write(buffer: ByteBuffer, includeSignature: Boolean) { + Encode.varInt(from.version, buffer) + Encode.varInt(from.stream, buffer) + if (from.pubkey == null) { + Encode.int32(0, buffer) + val empty = ByteArray(64) + buffer.put(empty) + buffer.put(empty) + if (from.version >= 3) { + Encode.varInt(0, buffer) + Encode.varInt(0, buffer) + } + } else { + Encode.int32(from.pubkey!!.behaviorBitfield, buffer) + buffer.put(from.pubkey!!.signingKey, 1, 64) + buffer.put(from.pubkey!!.encryptionKey, 1, 64) + if (from.version >= 3) { + Encode.varInt(from.pubkey!!.nonceTrialsPerByte, buffer) + Encode.varInt(from.pubkey!!.extraBytes, buffer) + } + } + if (type == MSG) { + buffer.put(to!!.ripe) + } + Encode.varInt(encodingCode, buffer) + Encode.varBytes(message, buffer) + if (type == MSG) { + if (to!!.has(Feature.DOES_ACK) && ackMessage != null) { + Encode.varBytes(Encode.bytes(ackMessage!!), buffer) + } else { + Encode.varInt(0, buffer) + } + } + if (includeSignature) { + val sig = signature + if (sig == null) { + Encode.varInt(0, buffer) + } else { + Encode.varBytes(sig, buffer) + } + } + } + + override fun write(out: OutputStream) { + write(out, true) + } + + override fun write(buffer: ByteBuffer) { + write(buffer, true) + } + + fun updateNextTry() { + if (to != null) { + if (nextTry == null) { + if (sent != null && to!!.has(Feature.DOES_ACK)) { + nextTry = UnixTime.now + ttl + retries++ + } + } else { + nextTry = nextTry!! + (1 shl retries) * ttl + retries++ + } + } + } + + val subject: String? + get() { + val s = Scanner(ByteArrayInputStream(message), "UTF-8") + val firstLine = s.nextLine() + if (encodingCode == EXTENDED.code) { + if (Message.TYPE == extendedData?.type) { + return (extendedData!!.content as? Message)?.subject + } else { + return null + } + } else if (encodingCode == SIMPLE.code) { + return firstLine.substring("Subject:".length).trim { it <= ' ' } + } else if (firstLine.length > 50) { + return firstLine.substring(0, 50).trim { it <= ' ' } + "..." + } else { + return firstLine + } + } + + val text: String? + get() { + if (encodingCode == EXTENDED.code) { + if (Message.TYPE == extendedData?.type) { + return (extendedData?.content as Message?)?.body + } else { + return null + } + } else { + val text = String(message) + if (encodingCode == SIMPLE.code) { + return text.substring(text.indexOf("\nBody:") + 6) + } + return text + } + } + + fun <T : ExtendedEncoding.ExtendedType> getExtendedData(type: Class<T>): T? { + val extendedData = extendedData ?: return null + if (type.isInstance(extendedData.content)) { + @Suppress("UNCHECKED_CAST") + return extendedData.content as T + } + return null + } + + val parents: List<InventoryVector> + get() { + val extendedData = extendedData ?: return emptyList() + if (Message.TYPE == extendedData.type) { + return (extendedData.content as Message).parents + } else { + return emptyList() + } + } + + val files: List<Attachment> + get() { + val extendedData = extendedData ?: return emptyList() + if (Message.TYPE == extendedData.type) { + return (extendedData.content as Message).files + } else { + return emptyList() + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Plaintext) return false + return encoding == other.encoding && + from == other.from && + Arrays.equals(message, other.message) && + ackMessage == other.ackMessage && + Arrays.equals(to?.ripe, other.to?.ripe) && + Arrays.equals(signature, other.signature) && + status == other.status && + sent == other.sent && + received == other.received && + labels == other.labels + } + + override fun hashCode(): Int { + return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels) + } + + fun addLabels(vararg labels: Label) { + Collections.addAll(this.labels, *labels) + } + + fun addLabels(labels: Collection<Label>?) { + if (labels != null) { + this.labels.addAll(labels) + } + } + + fun removeLabel(type: Label.Type) { + labels.removeAll { it.type == type } + } + + fun isUnread(): Boolean { + return labels.any { it.type == Label.Type.UNREAD } + } + + override fun toString(): String { + val subject = subject + if (subject?.isNotEmpty() ?: false) { + return subject!! + } else { + return Strings.hex( + initialHash ?: return super.toString() + ) + } + } + + enum class Encoding constructor(code: Long) { + IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3); + + var code: Long = 0 + internal set + + init { + this.code = code + } + + companion object { + + @JvmStatic fun fromCode(code: Long): Encoding? { + for (e in values()) { + if (e.code == code) { + return e + } + } + return null + } + } + } + + enum class Status { + DRAFT, + // For sent messages + PUBKEY_REQUESTED, + DOING_PROOF_OF_WORK, + SENT, + SENT_ACKNOWLEDGED, + RECEIVED + } + + enum class Type { + MSG, BROADCAST + } + + class Builder(internal val type: Type) { + internal var id: Any? = null + internal var inventoryVector: InventoryVector? = null + internal var from: BitmessageAddress? = null + internal var to: BitmessageAddress? = null + private var addressVersion: Long = 0 + private var stream: Long = 0 + private var behaviorBitfield: Int = 0 + private var publicSigningKey: ByteArray? = null + private var publicEncryptionKey: ByteArray? = null + private var nonceTrialsPerByte: Long = 0 + private var extraBytes: Long = 0 + private var destinationRipe: ByteArray? = null + internal var encoding: Long = 0 + internal var message = ByteArray(0) + internal var ackData: ByteArray? = null + internal var ackMessage: ByteArray? = null + internal var signature: ByteArray? = null + internal var sent: Long? = null + internal var received: Long? = null + internal var status: Status? = null + internal val labels = LinkedHashSet<Label>() + internal var ttl: Long = 0 + internal var retries: Int = 0 + internal var nextTry: Long? = null + internal var conversation: UUID? = null + + fun id(id: Any): Builder { + this.id = id + return this + } + + fun IV(iv: InventoryVector?): Builder { + this.inventoryVector = iv + return this + } + + fun from(address: BitmessageAddress): Builder { + from = address + return this + } + + fun to(address: BitmessageAddress?): Builder { + if (address != null) { + if (type != MSG && to != null) + throw IllegalArgumentException("recipient address only allowed for msg") + to = address + } + return this + } + + fun addressVersion(addressVersion: Long): Builder { + this.addressVersion = addressVersion + return this + } + + fun stream(stream: Long): Builder { + this.stream = stream + return this + } + + fun behaviorBitfield(behaviorBitfield: Int): Builder { + this.behaviorBitfield = behaviorBitfield + return this + } + + fun publicSigningKey(publicSigningKey: ByteArray): Builder { + this.publicSigningKey = publicSigningKey + return this + } + + fun publicEncryptionKey(publicEncryptionKey: ByteArray): Builder { + this.publicEncryptionKey = publicEncryptionKey + return this + } + + fun nonceTrialsPerByte(nonceTrialsPerByte: Long): Builder { + this.nonceTrialsPerByte = nonceTrialsPerByte + return this + } + + fun extraBytes(extraBytes: Long): Builder { + this.extraBytes = extraBytes + return this + } + + fun destinationRipe(ripe: ByteArray?): Builder { + if (type != MSG && ripe != null) throw IllegalArgumentException("ripe only allowed for msg") + this.destinationRipe = ripe + return this + } + + fun encoding(encoding: Encoding): Builder { + this.encoding = encoding.code + return this + } + + fun encoding(encoding: Long): Builder { + this.encoding = encoding + return this + } + + fun message(message: ExtendedEncoding): Builder { + this.encoding = EXTENDED.code + this.message = message.zip() + return this + } + + fun message(subject: String, message: String): Builder { + try { + this.encoding = SIMPLE.code + this.message = "Subject:$subject\nBody:$message".toByteArray(charset("UTF-8")) + } catch (e: UnsupportedEncodingException) { + throw ApplicationException(e) + } + return this + } + + fun message(message: ByteArray): Builder { + this.message = message + return this + } + + fun ackMessage(ack: ByteArray?): Builder { + if (type != MSG && ack != null) throw IllegalArgumentException("ackMessage only allowed for msg") + this.ackMessage = ack + return this + } + + fun ackData(ackData: ByteArray?): Builder { + if (type != MSG && ackData != null) + throw IllegalArgumentException("ackMessage only allowed for msg") + this.ackData = ackData + return this + } + + fun signature(signature: ByteArray): Builder { + this.signature = signature + return this + } + + fun sent(sent: Long?): Builder { + this.sent = sent + return this + } + + fun received(received: Long?): Builder { + this.received = received + return this + } + + fun status(status: Status): Builder { + this.status = status + return this + } + + fun labels(labels: Collection<Label>): Builder { + this.labels.addAll(labels) + return this + } + + fun ttl(ttl: Long): Builder { + this.ttl = ttl + return this + } + + fun retries(retries: Int): Builder { + this.retries = retries + return this + } + + fun nextTry(nextTry: Long?): Builder { + this.nextTry = nextTry + return this + } + + fun conversation(id: UUID): Builder { + this.conversation = id + return this + } + + fun build(): Plaintext { + if (from == null) { + from = BitmessageAddress(Factory.createPubkey( + addressVersion, + stream, + publicSigningKey!!, + publicEncryptionKey!!, + nonceTrialsPerByte, + extraBytes, + behaviorBitfield + )) + } + if (to == null && type != Type.BROADCAST && destinationRipe != null) { + to = BitmessageAddress(0, 0, destinationRipe!!) + } + if (type == MSG && ackMessage == null && ackData == null) { + ackData = cryptography().randomBytes(Msg.ACK_LENGTH) + } + if (ttl <= 0) { + ttl = TTL.msg + } + return Plaintext(this) + } + } + + companion object { + + @JvmStatic fun read(type: Type, `in`: InputStream): Plaintext { + return readWithoutSignature(type, `in`) + .signature(Decode.varBytes(`in`)) + .received(UnixTime.now) + .build() + } + + @JvmStatic fun readWithoutSignature(type: Type, `in`: InputStream): Plaintext.Builder { + val version = Decode.varInt(`in`) + return Builder(type) + .addressVersion(version) + .stream(Decode.varInt(`in`)) + .behaviorBitfield(Decode.int32(`in`)) + .publicSigningKey(Decode.bytes(`in`, 64)) + .publicEncryptionKey(Decode.bytes(`in`, 64)) + .nonceTrialsPerByte(if (version >= 3) Decode.varInt(`in`) else 0) + .extraBytes(if (version >= 3) Decode.varInt(`in`) else 0) + .destinationRipe(if (type == MSG) Decode.bytes(`in`, 20) else null) + .encoding(Decode.varInt(`in`)) + .message(Decode.varBytes(`in`)) + .ackMessage(if (type == MSG) Decode.varBytes(`in`) else null) + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java b/core/src/main/kotlin/ch/dissem/bitmessage/entity/PlaintextHolder.kt similarity index 80% rename from core/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java rename to core/src/main/kotlin/ch/dissem/bitmessage/entity/PlaintextHolder.kt index 3133cab..e890b13 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/PlaintextHolder.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/PlaintextHolder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,8 @@ * limitations under the License. */ -package ch.dissem.bitmessage.entity; +package ch.dissem.bitmessage.entity -public interface PlaintextHolder { - Plaintext getPlaintext(); +interface PlaintextHolder { + val plaintext: Plaintext? } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Streamable.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Streamable.kt new file mode 100644 index 0000000..b8ad391 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Streamable.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017 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 java.io.OutputStream +import java.io.Serializable +import java.nio.ByteBuffer + +/** + * An object that can be written to an [OutputStream] + */ +interface Streamable : Serializable { + fun write(out: OutputStream) + + fun write(buffer: ByteBuffer) +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java b/core/src/main/kotlin/ch/dissem/bitmessage/entity/VerAck.kt similarity index 63% rename from core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java rename to core/src/main/kotlin/ch/dissem/bitmessage/entity/VerAck.kt index 3d30f32..268dabe 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/VerAck.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,30 +14,27 @@ * limitations under the License. */ -package ch.dissem.bitmessage.entity; +package ch.dissem.bitmessage.entity -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; +import java.io.OutputStream +import java.nio.ByteBuffer /** * The 'verack' command answers a 'version' command, accepting the other node's version. */ -public class VerAck implements MessagePayload { - private static final long serialVersionUID = -4302074845199181687L; +class VerAck : MessagePayload { - @Override - public Command getCommand() { - return Command.VERACK; - } + override val command: MessagePayload.Command = MessagePayload.Command.VERACK - @Override - public void write(OutputStream stream) throws IOException { + override fun write(out: OutputStream) { // 'verack' doesn't have any payload, so there is nothing to write } - @Override - public void write(ByteBuffer buffer) { + override fun write(buffer: ByteBuffer) { // 'verack' doesn't have any payload, so there is nothing to write } + + companion object { + private val serialVersionUID = -4302074845199181687L + } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Version.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Version.kt new file mode 100644 index 0000000..5f65bcc --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Version.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2017 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.BitmessageContext +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.utils.Encode +import ch.dissem.bitmessage.utils.UnixTime +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * The 'version' command advertises this node's latest supported protocol version upon initiation. + */ +class Version constructor( + /** + * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's + * version is lower but continue with the connection if it is higher. + */ + val version: Int = BitmessageContext.CURRENT_VERSION, + + /** + * bitfield of features to be enabled for this connection + */ + val services: Long = Version.Service.getServiceFlag(Version.Service.NODE_NETWORK), + + /** + * standard UNIX timestamp in seconds + */ + val timestamp: Long = UnixTime.now, + + /** + * The network address of the node receiving this message (not including the time or stream number) + */ + val addrRecv: NetworkAddress, + + /** + * The network address of the node emitting this message (not including the time or stream number and the ip itself + * is ignored by the receiver) + */ + val addrFrom: NetworkAddress, + + /** + * Random nonce used to detect connections to self. + */ + val nonce: Long, + + /** + * User Agent (0x00 if string is 0 bytes long). Sending nodes must not include a user_agent longer than 5000 bytes. + */ + val userAgent: String, + + /** + * The stream numbers that the emitting node is interested in. Sending nodes must not include more than 160000 + * stream numbers. + */ + val streams: LongArray = longArrayOf(1) +) : MessagePayload { + + fun provides(service: Service?): Boolean { + return service != null && service.isEnabled(services) + } + + override val command: MessagePayload.Command = MessagePayload.Command.VERSION + + override fun write(out: OutputStream) { + Encode.int32(version, out) + Encode.int64(services, out) + Encode.int64(timestamp, out) + addrRecv.write(out, true) + addrFrom.write(out, true) + Encode.int64(nonce, out) + Encode.varString(userAgent, out) + Encode.varIntList(streams, out) + } + + override fun write(buffer: ByteBuffer) { + Encode.int32(version, buffer) + Encode.int64(services, buffer) + Encode.int64(timestamp, buffer) + addrRecv.write(buffer, true) + addrFrom.write(buffer, true) + Encode.int64(nonce, buffer) + Encode.varString(userAgent, buffer) + Encode.varIntList(streams, buffer) + } + + class Builder { + private var version: Int = 0 + private var services: Long = 0 + private var timestamp: Long = 0 + private var addrRecv: NetworkAddress? = null + private var addrFrom: NetworkAddress? = null + private var nonce: Long = 0 + private var userAgent: String? = null + private var streamNumbers: LongArray? = null + + fun defaults(clientNonce: Long): Builder { + version = BitmessageContext.CURRENT_VERSION + services = Service.getServiceFlag(Service.NODE_NETWORK) + timestamp = UnixTime.now + userAgent = "/Jabit:0.0.1/" + streamNumbers = longArrayOf(1) + nonce = clientNonce + return this + } + + fun version(version: Int): Builder { + this.version = version + return this + } + + fun services(vararg services: Service): Builder { + this.services = Service.getServiceFlag(*services) + return this + } + + fun services(services: Long): Builder { + this.services = services + return this + } + + fun timestamp(timestamp: Long): Builder { + this.timestamp = timestamp + return this + } + + fun addrRecv(addrRecv: NetworkAddress): Builder { + this.addrRecv = addrRecv + return this + } + + fun addrFrom(addrFrom: NetworkAddress): Builder { + this.addrFrom = addrFrom + return this + } + + fun nonce(nonce: Long): Builder { + this.nonce = nonce + return this + } + + fun userAgent(userAgent: String): Builder { + this.userAgent = userAgent + return this + } + + fun streams(vararg streamNumbers: Long): Builder { + this.streamNumbers = streamNumbers + return this + } + + fun build(): Version { + val addrRecv = this.addrRecv + val addrFrom = this.addrFrom + if (addrRecv == null || addrFrom == null) { + throw IllegalStateException("Receiving and sending address must be set") + } + + return Version( + version = version, + services = services, + timestamp = timestamp, + addrRecv = addrRecv, addrFrom = addrFrom, + nonce = nonce, + userAgent = userAgent ?: "/Jabit:0.0.1/", + streams = streamNumbers ?: longArrayOf(1) + ) + } + } + + enum class Service constructor(internal var flag: Long) { + // TODO: NODE_SSL(2); + NODE_NETWORK(1); + + fun isEnabled(flag: Long): Boolean { + return (flag and this.flag) != 0L + } + + companion object { + fun getServiceFlag(vararg services: Service): Long { + var flag: Long = 0 + for (service in services) { + flag = flag or service.flag + } + return flag + } + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Broadcast.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Broadcast.kt new file mode 100644 index 0000000..9cc5772 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Broadcast.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Encrypted +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST +import ch.dissem.bitmessage.entity.PlaintextHolder +import ch.dissem.bitmessage.exception.DecryptionFailedException +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.util.* + +/** + * Users who are subscribed to the sending address will see the message appear in their inbox. + * Broadcasts are version 4 or 5. + */ +abstract class Broadcast protected constructor(version: Long, override val stream: Long, protected var encrypted: CryptoBox?, override var plaintext: Plaintext?) : ObjectPayload(version), Encrypted, PlaintextHolder { + + override val isSigned: Boolean = true + + override var signature: ByteArray? + get() = plaintext?.signature + set(signature) { + plaintext?.signature = signature ?: throw IllegalStateException("no plaintext data available") + } + + override fun encrypt(publicKey: ByteArray) { + this.encrypted = CryptoBox(plaintext ?: throw IllegalStateException("no plaintext data available"), publicKey) + } + + fun encrypt() { + encrypt(cryptography().createPublicKey(plaintext?.from?.publicDecryptionKey ?: return)) + } + + @Throws(DecryptionFailedException::class) + override fun decrypt(privateKey: ByteArray) { + plaintext = Plaintext.read(BROADCAST, encrypted?.decrypt(privateKey) ?: return) + } + + @Throws(DecryptionFailedException::class) + fun decrypt(address: BitmessageAddress) { + decrypt(address.publicDecryptionKey) + } + + override val isDecrypted: Boolean + get() = plaintext != null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Broadcast) return false + return stream == other.stream && (encrypted == other.encrypted || plaintext == other.plaintext) + } + + override fun hashCode(): Int { + return Objects.hash(stream) + } + + companion object { + fun getVersion(address: BitmessageAddress): Long { + return if (address.version < 4) 4L else 5L + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/CryptoBox.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/CryptoBox.kt new file mode 100644 index 0000000..29f26fe --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/CryptoBox.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.Streamable +import ch.dissem.bitmessage.entity.valueobject.PrivateKey.Companion.PRIVATE_KEY_SIZE +import ch.dissem.bitmessage.exception.DecryptionFailedException +import ch.dissem.bitmessage.utils.* +import ch.dissem.bitmessage.utils.Singleton.cryptography +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + + +class CryptoBox : Streamable { + + private val initializationVector: ByteArray + private val curveType: Int + private val R: ByteArray + private val mac: ByteArray + private var encrypted: ByteArray + + constructor(data: Streamable, K: ByteArray) : this(Encode.bytes(data), K) + + constructor(data: ByteArray, K: ByteArray) { + 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 = cryptography().randomBytes(16) + + // 3. Generate a new random EC key pair with private key called r and public key called R. + val r = cryptography().randomBytes(PRIVATE_KEY_SIZE) + R = cryptography().createPublicKey(r) + // 4. Do an EC point multiply with public key K and private key r. This gives you public key P. + val P = cryptography().multiply(K, r) + val X = Points.getX(P) + // 5. Use the X component of public key P and calculate the SHA512 hash H. + val H = cryptography().sha512(X) + // 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. + val key_e = Arrays.copyOfRange(H, 0, 32) + val 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 = cryptography().crypt(true, data, key_e, initializationVector) + // 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. + mac = calculateMac(key_m) + + // The resulting data is: IV + R + cipher text + MAC + } + + private constructor(builder: Builder) { + initializationVector = builder.initializationVector!! + curveType = builder.curveType + R = cryptography().createPoint(builder.xComponent!!, builder.yComponent!!) + encrypted = builder.encrypted!! + mac = builder.mac!! + } + + /** + * @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 [https://bitmessage.org/wiki/Encryption.Decryption](https://bitmessage.org/wiki/Encryption.Decryption) + */ + @Throws(DecryptionFailedException::class) + fun decrypt(k: ByteArray): InputStream { + // 1. The private key used to decrypt is called k. + // 2. Do an EC point multiply with private key k and public key R. This gives you public key P. + val P = cryptography().multiply(R, k) + // 3. Use the X component of public key P and calculate the SHA512 hash H. + val H = cryptography().sha512(Arrays.copyOfRange(P, 1, 33)) + // 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. + val key_e = Arrays.copyOfRange(H, 0, 32) + val key_m = Arrays.copyOfRange(H, 32, 64) + + // 5. Calculate MAC' with HMACSHA256, using key_m as salt and IV + R + cipher text as data. + // 6. Compare MAC with MAC'. If not equal, decryption will fail. + if (!Arrays.equals(mac, calculateMac(key_m))) { + throw DecryptionFailedException() + } + + // 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 ByteArrayInputStream(cryptography().crypt(false, encrypted, key_e, initializationVector)) + } + + private fun calculateMac(key_m: ByteArray): ByteArray { + val macData = ByteArrayOutputStream() + writeWithoutMAC(macData) + return cryptography().mac(key_m, macData.toByteArray()) + } + + private fun writeWithoutMAC(out: OutputStream) { + out.write(initializationVector) + Encode.int16(curveType, out) + writeCoordinateComponent(out, Points.getX(R)) + writeCoordinateComponent(out, Points.getY(R)) + out.write(encrypted) + } + + private fun writeCoordinateComponent(out: OutputStream, x: ByteArray) { + val offset = Bytes.numberOfLeadingZeros(x) + val length = x.size - offset + Encode.int16(length, out) + out.write(x, offset, length) + } + + private fun writeCoordinateComponent(buffer: ByteBuffer, x: ByteArray) { + val offset = Bytes.numberOfLeadingZeros(x) + val length = x.size - offset + Encode.int16(length, buffer) + buffer.put(x, offset, length) + } + + override fun write(out: OutputStream) { + writeWithoutMAC(out) + out.write(mac) + } + + override fun write(buffer: ByteBuffer) { + buffer.put(initializationVector) + Encode.int16(curveType, buffer) + writeCoordinateComponent(buffer, Points.getX(R)) + writeCoordinateComponent(buffer, Points.getY(R)) + buffer.put(encrypted) + buffer.put(mac) + } + + class Builder { + internal var initializationVector: ByteArray? = null + internal var curveType: Int = 0 + internal var xComponent: ByteArray? = null + internal var yComponent: ByteArray? = null + internal var encrypted: ByteArray? = null + internal var mac: ByteArray? = null + + fun IV(initializationVector: ByteArray): Builder { + this.initializationVector = initializationVector + return this + } + + fun curveType(curveType: Int): Builder { + if (curveType != 0x2CA) LOG.trace("Unexpected curve type " + curveType) + this.curveType = curveType + return this + } + + fun X(xComponent: ByteArray): Builder { + this.xComponent = xComponent + return this + } + + fun Y(yComponent: ByteArray): Builder { + this.yComponent = yComponent + return this + } + + fun encrypted(encrypted: ByteArray): Builder { + this.encrypted = encrypted + return this + } + + fun MAC(mac: ByteArray): Builder { + this.mac = mac + return this + } + + fun build(): CryptoBox { + return CryptoBox(this) + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(CryptoBox::class.java) + + @JvmStatic fun read(stream: InputStream, length: Int): CryptoBox { + val counter = AccessCounter() + return Builder() + .IV(Decode.bytes(stream, 16, counter)) + .curveType(Decode.uint16(stream, counter)) + .X(Decode.shortVarBytes(stream, counter)) + .Y(Decode.shortVarBytes(stream, counter)) + .encrypted(Decode.bytes(stream, length - counter.length() - 32)) + .MAC(Decode.bytes(stream, 32)) + .build() + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/GenericPayload.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/GenericPayload.kt new file mode 100644 index 0000000..10f0448 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/GenericPayload.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.utils.Decode +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +/** + * In cases we don't know what to do with an object, we just store its bytes and send it again - we don't really + * have to know what it is. + */ +class GenericPayload(version: Long, override val stream: Long, val data: ByteArray) : ObjectPayload(version) { + + override val type: ObjectType? = null + + override fun write(out: OutputStream) { + out.write(data) + } + + override fun write(buffer: ByteBuffer) { + buffer.put(data) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GenericPayload) return false + + if (stream != other.stream) return false + return Arrays.equals(data, other.data) + } + + override fun hashCode(): Int { + var result = (stream xor stream.ushr(32)).toInt() + result = 31 * result + Arrays.hashCode(data) + return result + } + + companion object { + @JvmStatic fun read(version: Long, stream: Long, `is`: InputStream, length: Int): GenericPayload { + return GenericPayload(version, stream, Decode.bytes(`is`, length)) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/GetPubkey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/GetPubkey.kt new file mode 100644 index 0000000..fc6375f --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/GetPubkey.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.utils.Decode +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * Request for a public key. + */ +class GetPubkey : ObjectPayload { + + override val type: ObjectType = ObjectType.GET_PUBKEY + override val stream: Long + + /** + * @return an array of bytes that represent either the ripe, or the tag of an address, depending on the + * * address version. + */ + val ripeTag: ByteArray + + constructor(address: BitmessageAddress) : super(address.version) { + this.stream = address.stream + this.ripeTag = if (address.version < 4) address.ripe else + address.tag ?: throw IllegalStateException("Address of version 4 without tag shouldn't exist!") + } + + private constructor(version: Long, stream: Long, ripeOrTag: ByteArray) : super(version) { + this.stream = stream + this.ripeTag = ripeOrTag + } + + override fun write(out: OutputStream) { + out.write(ripeTag) + } + + override fun write(buffer: ByteBuffer) { + buffer.put(ripeTag) + } + + companion object { + @JvmStatic fun read(`is`: InputStream, stream: Long, length: Int, version: Long): GetPubkey { + return GetPubkey(version, stream, Decode.bytes(`is`, length)) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Msg.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Msg.kt new file mode 100644 index 0000000..c2ebd8b --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Msg.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.Encrypted +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.PlaintextHolder +import ch.dissem.bitmessage.exception.DecryptionFailedException +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * Used for person-to-person messages. + */ +class Msg : ObjectPayload, Encrypted, PlaintextHolder { + + override val stream: Long + private var encrypted: CryptoBox? + override var plaintext: Plaintext? + private set + + private constructor(stream: Long, encrypted: CryptoBox) : super(1) { + this.stream = stream + this.encrypted = encrypted + this.plaintext = null + } + + constructor(plaintext: Plaintext) : super(1) { + this.stream = plaintext.stream + this.encrypted = null + this.plaintext = plaintext + } + + override val type: ObjectType = ObjectType.MSG + + override val isSigned: Boolean = true + + override fun writeBytesToSign(out: OutputStream) { + plaintext?.write(out, false) ?: throw IllegalStateException("no plaintext data available") + } + + override var signature: ByteArray? + get() = plaintext?.signature + set(signature) { + plaintext?.signature = signature ?: throw IllegalStateException("no plaintext data available") + } + + override fun encrypt(publicKey: ByteArray) { + this.encrypted = CryptoBox(plaintext ?: throw IllegalStateException("no plaintext data available"), publicKey) + } + + @Throws(DecryptionFailedException::class) + override fun decrypt(privateKey: ByteArray) { + plaintext = Plaintext.read(MSG, encrypted!!.decrypt(privateKey)) + } + + override val isDecrypted: Boolean + get() = plaintext != null + + override fun write(out: OutputStream) { + encrypted?.write(out) ?: throw IllegalStateException("Msg must be signed and encrypted before writing it.") + } + + override fun write(buffer: ByteBuffer) { + encrypted?.write(buffer) ?: throw IllegalStateException("Msg must be signed and encrypted before writing it.") + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Msg) return false + if (!super.equals(other)) return false + + return stream == other.stream && (encrypted == other.encrypted || plaintext == other.plaintext) + } + + override fun hashCode(): Int { + return stream.toInt() + } + + companion object { + val ACK_LENGTH = 32 + + @JvmStatic fun read(`in`: InputStream, stream: Long, length: Int): Msg { + return Msg(stream, CryptoBox.read(`in`, length)) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/ObjectPayload.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/ObjectPayload.kt new file mode 100644 index 0000000..5650652 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/ObjectPayload.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Streamable +import java.io.OutputStream + +/** + * The payload of an 'object' command. This is shared by the network. + */ +abstract class ObjectPayload protected constructor(val version: Long) : Streamable { + + abstract val type: ObjectType? + + abstract val stream: Long + + open val isSigned: Boolean = false + + open fun writeBytesToSign(out: OutputStream) { + // nothing to do + } + + /** + * @return the ECDSA signature which, as of protocol v3, covers the object header starting with the time, + * * appended with the data described in this table down to the extra_bytes. Therefore, this must + * * be checked and set in the [ObjectMessage] object. + */ + open var signature: ByteArray? = null +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/ObjectType.kt similarity index 64% rename from core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java rename to core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/ObjectType.kt index 06ea92f..ec7065a 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectType.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/ObjectType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,31 +14,20 @@ * limitations under the License. */ -package ch.dissem.bitmessage.entity.payload; +package ch.dissem.bitmessage.entity.payload /** * Known types for 'object' messages. Must not be used where an unknown type must be resent. */ -public enum ObjectType { +enum class ObjectType constructor(val number: Long) { GET_PUBKEY(0), PUBKEY(1), MSG(2), BROADCAST(3); - int number; - - ObjectType(int number) { - this.number = number; - } - - public static ObjectType fromNumber(long number) { - for (ObjectType type : values()) { - if (type.number == number) return type; + companion object { + @JvmStatic fun fromNumber(number: Long): ObjectType? { + return values().firstOrNull { it.number == number } } - return null; - } - - public long getNumber() { - return number; } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Pubkey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Pubkey.kt new file mode 100644 index 0000000..87e1355 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Pubkey.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +/** + * Public keys for signing and encryption, the answer to a 'getpubkey' request. + */ +abstract class Pubkey protected constructor(version: Long) : ObjectPayload(version) { + + override val type: ObjectType = ObjectType.PUBKEY + + abstract val signingKey: ByteArray + + abstract val encryptionKey: ByteArray + + abstract val behaviorBitfield: Int + + val ripe: ByteArray by lazy { cryptography().ripemd160(cryptography().sha512(signingKey, encryptionKey)) } + + open val nonceTrialsPerByte: Long = NETWORK_NONCE_TRIALS_PER_BYTE + + open val extraBytes: Long = NETWORK_EXTRA_BYTES + + open fun writeUnencrypted(out: OutputStream) { + write(out) + } + + open fun writeUnencrypted(buffer: ByteBuffer) { + write(buffer) + } + + /** + * Bits 0 through 29 are yet undefined + */ + enum class Feature constructor(bitNumber: Int) { + /** + * Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg + * messages bound for them. + */ + INCLUDE_DESTINATION(30), + /** + * If true, the receiving node does send acknowledgements (rather than dropping them). + */ + DOES_ACK(31); + + // The Bitmessage Protocol Specification starts counting at the most significant bit, + // thus the slightly awkward calculation. + // https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features + private val bit: Int = 1 shl 31 - bitNumber + + fun isActive(bitfield: Int): Boolean { + return bitfield and bit != 0 + } + + companion object { + @JvmStatic fun bitfield(vararg features: Feature): Int { + var bits = 0 + for (feature in features) { + bits = bits or feature.bit + } + return bits + } + + @JvmStatic fun features(bitfield: Int): Array<Feature> { + val features = ArrayList<Feature>(Feature.values().size) + for (feature in Feature.values()) { + if (bitfield and feature.bit != 0) { + features.add(feature) + } + } + return features.toTypedArray() + } + } + } + + companion object { + @JvmField val LATEST_VERSION: Long = 4 + + fun getRipe(publicSigningKey: ByteArray, publicEncryptionKey: ByteArray): ByteArray { + return cryptography().ripemd160(cryptography().sha512(publicSigningKey, publicEncryptionKey)) + } + + fun add0x04(key: ByteArray): ByteArray { + if (key.size == 65) return key + val result = ByteArray(65) + result[0] = 4 + System.arraycopy(key, 0, result, 1, 64) + return result + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V2Pubkey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V2Pubkey.kt new file mode 100644 index 0000000..c1c7253 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V2Pubkey.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.utils.Decode +import ch.dissem.bitmessage.utils.Encode +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * A version 2 public key. + */ +open class V2Pubkey constructor(version: Long, override val stream: Long, override val behaviorBitfield: Int, signingKey: ByteArray, encryptionKey: ByteArray) : Pubkey(version) { + + override val signingKey: ByteArray = if (signingKey.size == 64) add0x04(signingKey) else signingKey + override val encryptionKey: ByteArray = if (encryptionKey.size == 64) add0x04(encryptionKey) else encryptionKey + + override fun write(out: OutputStream) { + Encode.int32(behaviorBitfield, out) + out.write(signingKey, 1, 64) + out.write(encryptionKey, 1, 64) + } + + override fun write(buffer: ByteBuffer) { + Encode.int32(behaviorBitfield, buffer) + buffer.put(signingKey, 1, 64) + buffer.put(encryptionKey, 1, 64) + } + + class Builder { + internal var streamNumber: Long = 0 + internal var behaviorBitfield: Int = 0 + internal var publicSigningKey: ByteArray? = null + internal var publicEncryptionKey: ByteArray? = null + + fun stream(streamNumber: Long): Builder { + this.streamNumber = streamNumber + return this + } + + fun behaviorBitfield(behaviorBitfield: Int): Builder { + this.behaviorBitfield = behaviorBitfield + return this + } + + fun publicSigningKey(publicSigningKey: ByteArray): Builder { + this.publicSigningKey = publicSigningKey + return this + } + + fun publicEncryptionKey(publicEncryptionKey: ByteArray): Builder { + this.publicEncryptionKey = publicEncryptionKey + return this + } + + fun build(): V2Pubkey { + return V2Pubkey( + version = 2, + stream = streamNumber, + behaviorBitfield = behaviorBitfield, + signingKey = add0x04(publicSigningKey!!), + encryptionKey = add0x04(publicEncryptionKey!!) + ) + } + } + + companion object { + @JvmStatic fun read(`in`: InputStream, stream: Long): V2Pubkey { + return V2Pubkey( + version = 2, + stream = stream, + behaviorBitfield = Decode.uint32(`in`).toInt(), + signingKey = Decode.bytes(`in`, 64), + encryptionKey = Decode.bytes(`in`, 64) + ) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V3Pubkey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V3Pubkey.kt new file mode 100644 index 0000000..53faed8 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V3Pubkey.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.utils.Decode +import ch.dissem.bitmessage.utils.Encode +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +/** + * A version 3 public key. + */ +class V3Pubkey protected constructor( + version: Long, stream: Long, behaviorBitfield: Int, + signingKey: ByteArray, encryptionKey: ByteArray, + override val nonceTrialsPerByte: Long, + override val extraBytes: Long, + override var signature: ByteArray? = null +) : V2Pubkey(version, stream, behaviorBitfield, signingKey, encryptionKey) { + + override fun write(out: OutputStream) { + writeBytesToSign(out) + Encode.varBytes( + signature ?: throw IllegalStateException("signature not available"), + out + ) + } + + override fun write(buffer: ByteBuffer) { + super.write(buffer) + Encode.varInt(nonceTrialsPerByte, buffer) + Encode.varInt(extraBytes, buffer) + Encode.varBytes( + signature ?: throw IllegalStateException("signature not available"), + buffer + ) + } + + override val isSigned: Boolean = true + + override fun writeBytesToSign(out: OutputStream) { + super.write(out) + Encode.varInt(nonceTrialsPerByte, out) + Encode.varInt(extraBytes, out) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is V3Pubkey) return false + return nonceTrialsPerByte == other.nonceTrialsPerByte && + extraBytes == other.extraBytes && + stream == other.stream && + behaviorBitfield == other.behaviorBitfield && + Arrays.equals(signingKey, other.signingKey) && + Arrays.equals(encryptionKey, other.encryptionKey) + } + + override fun hashCode(): Int { + return Objects.hash(nonceTrialsPerByte, extraBytes) + } + + class Builder { + private var streamNumber: Long = 0 + private var behaviorBitfield: Int = 0 + private var publicSigningKey: ByteArray? = null + private var publicEncryptionKey: ByteArray? = null + private var nonceTrialsPerByte: Long = 0 + private var extraBytes: Long = 0 + private var signature = ByteArray(0) + + fun stream(streamNumber: Long): Builder { + this.streamNumber = streamNumber + return this + } + + fun behaviorBitfield(behaviorBitfield: Int): Builder { + this.behaviorBitfield = behaviorBitfield + return this + } + + fun publicSigningKey(publicSigningKey: ByteArray): Builder { + this.publicSigningKey = publicSigningKey + return this + } + + fun publicEncryptionKey(publicEncryptionKey: ByteArray): Builder { + this.publicEncryptionKey = publicEncryptionKey + return this + } + + fun nonceTrialsPerByte(nonceTrialsPerByte: Long): Builder { + this.nonceTrialsPerByte = nonceTrialsPerByte + return this + } + + fun extraBytes(extraBytes: Long): Builder { + this.extraBytes = extraBytes + return this + } + + fun signature(signature: ByteArray): Builder { + this.signature = signature + return this + } + + fun build(): V3Pubkey { + return V3Pubkey( + version = 3, + stream = streamNumber, + behaviorBitfield = behaviorBitfield, + signingKey = publicSigningKey!!, + encryptionKey = publicEncryptionKey!!, + nonceTrialsPerByte = nonceTrialsPerByte, + extraBytes = extraBytes, + signature = signature + ) + } + } + + companion object { + @JvmStatic fun read(`is`: InputStream, stream: Long): V3Pubkey { + return V3Pubkey( + version = 3, + stream = stream, + behaviorBitfield = Decode.int32(`is`), + signingKey = Decode.bytes(`is`, 64), + encryptionKey = Decode.bytes(`is`, 64), + nonceTrialsPerByte = Decode.varInt(`is`), + extraBytes = Decode.varInt(`is`), + signature = Decode.varBytes(`is`) + ) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V4Broadcast.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V4Broadcast.kt new file mode 100644 index 0000000..e270754 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V4Broadcast.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Plaintext + +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * Users who are subscribed to the sending address will see the message appear in their inbox. + * Broadcasts are version 4 or 5. + */ +open class V4Broadcast : Broadcast { + + override val type: ObjectType = ObjectType.BROADCAST + + protected constructor(version: Long, stream: Long, encrypted: CryptoBox?, plaintext: Plaintext?) : super(version, stream, encrypted, plaintext) + + constructor(senderAddress: BitmessageAddress, plaintext: Plaintext) : super(4, senderAddress.stream, null, plaintext) { + if (senderAddress.version >= 4) + throw IllegalArgumentException("Address version 3 or older expected, but was " + senderAddress.version) + } + + + override fun writeBytesToSign(out: OutputStream) { + plaintext?.write(out, false) ?: throw IllegalStateException("no plaintext data available") + } + + override fun write(out: OutputStream) { + encrypted?.write(out) ?: throw IllegalStateException("broadcast not encrypted") + } + + override fun write(buffer: ByteBuffer) { + encrypted?.write(buffer) ?: throw IllegalStateException("broadcast not encrypted") + } + + companion object { + @JvmStatic fun read(`in`: InputStream, stream: Long, length: Int): V4Broadcast { + return V4Broadcast(4, stream, CryptoBox.read(`in`, length), null) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V4Pubkey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V4Pubkey.kt new file mode 100644 index 0000000..1fc1196 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V4Pubkey.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Encrypted +import ch.dissem.bitmessage.exception.DecryptionFailedException +import ch.dissem.bitmessage.utils.Decode +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +/** + * A version 4 public key. When version 4 pubkeys are created, most of the data in the pubkey is encrypted. This is + * done in such a way that only someone who has the Bitmessage address which corresponds to a pubkey can decrypt and + * use that pubkey. This prevents people from gathering pubkeys sent around the network and using the data from them + * to create messages to be used in spam or in flooding attacks. + */ +class V4Pubkey : Pubkey, Encrypted { + + override val stream: Long + val tag: ByteArray + private var encrypted: CryptoBox? = null + private var decrypted: V3Pubkey? = null + + private constructor(stream: Long, tag: ByteArray, encrypted: CryptoBox) : super(4) { + this.stream = stream + this.tag = tag + this.encrypted = encrypted + } + + constructor(decrypted: V3Pubkey) : super(4) { + this.stream = decrypted.stream + this.decrypted = decrypted + this.tag = BitmessageAddress.calculateTag(4, decrypted.stream, decrypted.ripe) + } + + override fun encrypt(publicKey: ByteArray) { + if (signature == null) throw IllegalStateException("Pubkey must be signed before encryption.") + this.encrypted = CryptoBox(decrypted ?: throw IllegalStateException("no plaintext pubkey data available"), publicKey) + } + + @Throws(DecryptionFailedException::class) + override fun decrypt(privateKey: ByteArray) { + decrypted = V3Pubkey.read(encrypted?.decrypt(privateKey) ?: throw IllegalStateException("no encrypted data available"), stream) + } + + override val isDecrypted: Boolean + get() = decrypted != null + + override fun write(out: OutputStream) { + out.write(tag) + encrypted?.write(out) ?: throw IllegalStateException("pubkey is encrypted") + } + + override fun write(buffer: ByteBuffer) { + buffer.put(tag) + encrypted?.write(buffer) ?: throw IllegalStateException("pubkey is encrypted") + } + + override fun writeUnencrypted(out: OutputStream) { + decrypted?.write(out) ?: throw IllegalStateException("pubkey is encrypted") + } + + override fun writeUnencrypted(buffer: ByteBuffer) { + decrypted?.write(buffer) ?: throw IllegalStateException("pubkey is encrypted") + } + + override fun writeBytesToSign(out: OutputStream) { + out.write(tag) + decrypted?.writeBytesToSign(out) ?: throw IllegalStateException("pubkey is encrypted") + } + + override val signingKey: ByteArray + get() = decrypted?.signingKey ?: throw IllegalStateException("pubkey is encrypted") + + override val encryptionKey: ByteArray + get() = decrypted?.encryptionKey ?: throw IllegalStateException("pubkey is encrypted") + + override val behaviorBitfield: Int + get() = decrypted?.behaviorBitfield ?: throw IllegalStateException("pubkey is encrypted") + + override var signature: ByteArray? + get() = decrypted?.signature + set(signature) { + decrypted?.signature = signature + } + + override val isSigned: Boolean = true + + override val nonceTrialsPerByte: Long + get() = decrypted?.nonceTrialsPerByte ?: throw IllegalStateException("pubkey is encrypted") + + override val extraBytes: Long + get() = decrypted?.extraBytes ?: throw IllegalStateException("pubkey is encrypted") + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is V4Pubkey) return false + + if (stream != other.stream) return false + if (!Arrays.equals(tag, other.tag)) return false + return !if (decrypted != null) decrypted != other.decrypted else other.decrypted != null + + } + + override fun hashCode(): Int { + var result = (stream xor stream.ushr(32)).toInt() + result = 31 * result + Arrays.hashCode(tag) + result = 31 * result + if (decrypted != null) decrypted!!.hashCode() else 0 + return result + } + + companion object { + @JvmStatic fun read(`in`: InputStream, stream: Long, length: Int, encrypted: Boolean): V4Pubkey { + if (encrypted) + return V4Pubkey(stream, + Decode.bytes(`in`, 32), + CryptoBox.read(`in`, length - 32)) + else + return V4Pubkey(V3Pubkey.read(`in`, stream)) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V5Broadcast.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V5Broadcast.kt new file mode 100644 index 0000000..68b7454 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/V5Broadcast.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2017 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.payload + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.utils.Decode + +import java.io.InputStream +import java.io.OutputStream + +/** + * Users who are subscribed to the sending address will see the message appear in their inbox. + */ +class V5Broadcast : V4Broadcast { + + val tag: ByteArray + + private constructor(stream: Long, tag: ByteArray, encrypted: CryptoBox) : super(5, stream, encrypted, null) { + this.tag = tag + } + + constructor(senderAddress: BitmessageAddress, plaintext: Plaintext) : super(5, senderAddress.stream, null, plaintext) { + if (senderAddress.version < 4) + throw IllegalArgumentException("Address version 4 (or newer) expected, but was " + senderAddress.version) + this.tag = senderAddress.tag ?: throw IllegalStateException("version 4 address without tag") + } + + override fun writeBytesToSign(out: OutputStream) { + out.write(tag) + super.writeBytesToSign(out) + } + + override fun write(out: OutputStream) { + out.write(tag) + super.write(out) + } + + companion object { + @JvmStatic fun read(`is`: InputStream, stream: Long, length: Int): V5Broadcast { + return V5Broadcast(stream, Decode.bytes(`is`, 32), CryptoBox.read(`is`, length - 32)) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.kt new file mode 100644 index 0000000..87d85b3 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017 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.valueobject + +import ch.dissem.msgpack.types.MPMap +import ch.dissem.msgpack.types.MPString +import ch.dissem.msgpack.types.MPType +import java.io.ByteArrayOutputStream +import java.io.Serializable +import java.util.zip.DeflaterOutputStream + +/** + * Extended encoding message object. + */ +data class ExtendedEncoding(val content: ExtendedEncoding.ExtendedType) : Serializable { + + val type: String? = content.type + + fun zip(): ByteArray { + ByteArrayOutputStream().use { out -> + DeflaterOutputStream(out).use { zipper -> content.pack().pack(zipper) } + return out.toByteArray() + } + } + + interface Unpacker<out T : ExtendedType> { + val type: String + + fun unpack(map: MPMap<MPString, MPType<*>>): T + } + + interface ExtendedType : Serializable { + val type: String + + fun pack(): MPMap<MPString, MPType<*>> + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/InventoryVector.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/InventoryVector.kt new file mode 100644 index 0000000..144211a --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/InventoryVector.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017 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.valueobject + +import ch.dissem.bitmessage.entity.Streamable +import ch.dissem.bitmessage.utils.Strings +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +data class InventoryVector constructor( + /** + * Hash of the object + */ + val hash: ByteArray) : Streamable { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is InventoryVector) return false + + return Arrays.equals(hash, other.hash) + } + + override fun hashCode(): Int { + return Arrays.hashCode(hash) + } + + override fun write(out: OutputStream) { + out.write(hash) + } + + override fun write(buffer: ByteBuffer) { + buffer.put(hash) + } + + override fun toString(): String { + return Strings.hex(hash) + } + + companion object { + @JvmStatic fun fromHash(hash: ByteArray?): InventoryVector? { + return InventoryVector( + hash ?: return null + ) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt new file mode 100644 index 0000000..6fd4fd6 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017 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.valueobject + +import java.io.Serializable +import java.util.* + +data class Label( + private val label: String, + val type: Label.Type? = null, + /** + * RGBA representation for the color. + */ + var color: Int = 0 +) : Serializable { + + var id: Any? = null + + override fun toString(): String { + return label + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Label) return false + return label == other.label + } + + override fun hashCode(): Int { + return Objects.hash(label) + } + + enum class Type { + INBOX, + BROADCAST, + DRAFT, + OUTBOX, + SENT, + UNREAD, + TRASH + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.kt new file mode 100644 index 0000000..829bbfa --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2017 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.valueobject + +import ch.dissem.bitmessage.entity.Streamable +import ch.dissem.bitmessage.entity.Version +import ch.dissem.bitmessage.utils.Encode +import ch.dissem.bitmessage.utils.UnixTime +import java.io.OutputStream +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketAddress +import java.nio.ByteBuffer +import java.util.* + +fun ip6(inetAddress: InetAddress): ByteArray { + val address = inetAddress.address + when (address.size) { + 16 -> { + return address + } + 4 -> { + val ip6 = ByteArray(16) + ip6[10] = 0xff.toByte() + ip6[11] = 0xff.toByte() + System.arraycopy(address, 0, ip6, 12, 4) + return ip6 + } + else -> throw IllegalArgumentException("Weird address " + inetAddress) + } +} + +/** + * A node's address. It's written in IPv6 format. + */ +data class NetworkAddress( + var time: Long, + + /** + * Stream number for this node + */ + val stream: Long, + + /** + * same service(s) listed in version + */ + val services: Long, + + /** + * IPv6 address. IPv4 addresses are written into the message as a 16 byte IPv4-mapped IPv6 address + * (12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of the IPv4 address). + */ + val IPv6: ByteArray, + val port: Int +) : Streamable { + + constructor(time: Long, stream: Long, services: Long = 1, socket: Socket) + : this(time, stream, services, ip6(socket.inetAddress), socket.port) + + constructor(time: Long, stream: Long, services: Long = 1, inetAddress: InetAddress, port: Int) + : this(time, stream, services, ip6(inetAddress), port) + + fun provides(service: Version.Service?): Boolean = service?.isEnabled(services) ?: false + + fun toInetAddress(): InetAddress { + return InetAddress.getByAddress(IPv6) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NetworkAddress) return false + + return port == other.port && Arrays.equals(IPv6, other.IPv6) + } + + override fun hashCode(): Int { + var result = Arrays.hashCode(IPv6) + result = 31 * result + port + return result + } + + override fun toString(): String { + return "[" + toInetAddress() + "]:" + port + } + + override fun write(out: OutputStream) { + write(out, false) + } + + fun write(out: OutputStream, light: Boolean) { + if (!light) { + Encode.int64(time, out) + Encode.int32(stream, out) + } + Encode.int64(services, out) + out.write(IPv6) + Encode.int16(port, out) + } + + override fun write(buffer: ByteBuffer) { + write(buffer, false) + } + + fun write(buffer: ByteBuffer, light: Boolean) { + if (!light) { + Encode.int64(time, buffer) + Encode.int32(stream, buffer) + } + Encode.int64(services, buffer) + buffer.put(IPv6) + Encode.int16(port, buffer) + } + + class Builder { + internal var time: Long? = null + internal var stream: Long = 0 + internal var services: Long = 1 + internal var ipv6: ByteArray? = null + internal var port: Int = 0 + + fun time(time: Long): Builder { + this.time = time + return this + } + + fun stream(stream: Long): Builder { + this.stream = stream + return this + } + + fun services(services: Long): Builder { + this.services = services + return this + } + + fun ip(inetAddress: InetAddress): Builder { + ipv6 = ip6(inetAddress) + return this + } + + fun ipv6(ipv6: ByteArray): Builder { + this.ipv6 = ipv6 + return this + } + + fun ipv6(p00: Int, p01: Int, p02: Int, p03: Int, + p04: Int, p05: Int, p06: Int, p07: Int, + p08: Int, p09: Int, p10: Int, p11: Int, + p12: Int, p13: Int, p14: Int, p15: Int): Builder { + this.ipv6 = byteArrayOf(p00.toByte(), p01.toByte(), p02.toByte(), p03.toByte(), p04.toByte(), p05.toByte(), p06.toByte(), p07.toByte(), p08.toByte(), p09.toByte(), p10.toByte(), p11.toByte(), p12.toByte(), p13.toByte(), p14.toByte(), p15.toByte()) + return this + } + + fun ipv4(p00: Int, p01: Int, p02: Int, p03: Int): Builder { + this.ipv6 = byteArrayOf(0.toByte(), 0.toByte(), 0x00.toByte(), 0x00.toByte(), 0.toByte(), 0.toByte(), 0x00.toByte(), 0x00.toByte(), 0.toByte(), 0.toByte(), 0xff.toByte(), 0xff.toByte(), p00.toByte(), p01.toByte(), p02.toByte(), p03.toByte()) + return this + } + + fun port(port: Int): Builder { + this.port = port + return this + } + + fun address(address: SocketAddress): Builder { + if (address is InetSocketAddress) { + ip(address.address) + port(address.port) + } else { + throw IllegalArgumentException("Unknown type of address: " + address.javaClass) + } + return this + } + + fun build(): NetworkAddress { + return NetworkAddress( + time ?: UnixTime.now, stream, services, ipv6!!, port + ) + } + } + + companion object { + @JvmField val ANY = NetworkAddress(time = 0, stream = 0, services = 0, IPv6 = ByteArray(16), port = 0) + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt new file mode 100644 index 0000000..cfc618a --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2017 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.valueobject + +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Streamable +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.Bytes +import ch.dissem.bitmessage.utils.Decode +import ch.dissem.bitmessage.utils.Encode +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.* +import java.nio.ByteBuffer +import java.util.* + +/** + * Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying + * [Pubkey] object. + */ +class PrivateKey : Streamable { + + val privateSigningKey: ByteArray + val privateEncryptionKey: ByteArray + + val pubkey: Pubkey + + constructor(shorter: Boolean, stream: Long, nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature) { + var privSK: ByteArray + var pubSK: ByteArray + var privEK: ByteArray + var pubEK: ByteArray + var ripe: ByteArray + do { + privSK = cryptography().randomBytes(PRIVATE_KEY_SIZE) + privEK = cryptography().randomBytes(PRIVATE_KEY_SIZE) + pubSK = cryptography().createPublicKey(privSK) + pubEK = cryptography().createPublicKey(privEK) + ripe = Pubkey.getRipe(pubSK, pubEK) + } while (ripe[0].toInt() != 0 || shorter && ripe[1].toInt() != 0) + this.privateSigningKey = privSK + this.privateEncryptionKey = privEK + this.pubkey = cryptography().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, + nonceTrialsPerByte, extraBytes, *features) + } + + constructor(privateSigningKey: ByteArray, privateEncryptionKey: ByteArray, pubkey: Pubkey) { + this.privateSigningKey = privateSigningKey + this.privateEncryptionKey = privateEncryptionKey + this.pubkey = pubkey + } + + constructor(address: BitmessageAddress, passphrase: String) : this(address.version, address.stream, passphrase) + + constructor(version: Long, stream: Long, passphrase: String) : this(Builder(version, stream, false).seed(passphrase).generate()) + + private constructor(builder: Builder) { + this.privateSigningKey = builder.privSK!! + this.privateEncryptionKey = builder.privEK!! + this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK!!, builder.pubEK!!, + InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES) + } + + private class Builder internal constructor(internal val version: Long, internal val stream: Long, internal val shorter: Boolean) { + + internal var seed: ByteArray? = null + internal var nextNonce: Long = 0 + + internal var privSK: ByteArray? = null + internal var privEK: ByteArray? = null + internal var pubSK: ByteArray? = null + internal var pubEK: ByteArray? = null + + internal fun seed(passphrase: String): Builder { + try { + seed = passphrase.toByteArray(charset("UTF-8")) + } catch (e: UnsupportedEncodingException) { + throw ApplicationException(e) + } + + return this + } + + internal fun generate(): Builder { + var signingKeyNonce = nextNonce + var encryptionKeyNonce = nextNonce + 1 + var ripe: ByteArray + do { + privEK = Bytes.truncate(cryptography().sha512(seed!!, Encode.varInt(encryptionKeyNonce)), 32) + privSK = Bytes.truncate(cryptography().sha512(seed!!, Encode.varInt(signingKeyNonce)), 32) + pubSK = cryptography().createPublicKey(privSK!!) + pubEK = cryptography().createPublicKey(privEK!!) + ripe = cryptography().ripemd160(cryptography().sha512(pubSK!!, pubEK!!)) + + signingKeyNonce += 2 + encryptionKeyNonce += 2 + } while (ripe[0].toInt() != 0 || shorter && ripe[1].toInt() != 0) + nextNonce = signingKeyNonce + return this + } + } + + override fun write(out: OutputStream) { + Encode.varInt(pubkey.version, out) + Encode.varInt(pubkey.stream, out) + val baos = ByteArrayOutputStream() + pubkey.writeUnencrypted(baos) + Encode.varInt(baos.size(), out) + out.write(baos.toByteArray()) + Encode.varBytes(privateSigningKey, out) + Encode.varBytes(privateEncryptionKey, out) + } + + + override fun write(buffer: ByteBuffer) { + Encode.varInt(pubkey.version, buffer) + Encode.varInt(pubkey.stream, buffer) + try { + val baos = ByteArrayOutputStream() + pubkey.writeUnencrypted(baos) + Encode.varBytes(baos.toByteArray(), buffer) + } catch (e: IOException) { + throw ApplicationException(e) + } + + Encode.varBytes(privateSigningKey, buffer) + Encode.varBytes(privateEncryptionKey, buffer) + } + + companion object { + @JvmField val PRIVATE_KEY_SIZE = 32 + + @JvmStatic fun deterministic(passphrase: String, numberOfAddresses: Int, version: Long, stream: Long, shorter: Boolean): List<PrivateKey> { + val result = ArrayList<PrivateKey>(numberOfAddresses) + val builder = Builder(version, stream, shorter).seed(passphrase) + for (i in 0..numberOfAddresses - 1) { + builder.generate() + result.add(PrivateKey(builder)) + } + return result + } + + @JvmStatic fun read(`is`: InputStream): PrivateKey { + val version = Decode.varInt(`is`).toInt() + val stream = Decode.varInt(`is`) + val len = Decode.varInt(`is`).toInt() + val pubkey = Factory.readPubkey(version.toLong(), stream, `is`, len, false) ?: throw ApplicationException("Unknown pubkey version encountered") + val signingKey = Decode.varBytes(`is`) + val encryptionKey = Decode.varBytes(`is`) + return PrivateKey(signingKey, encryptionKey, pubkey) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.kt new file mode 100644 index 0000000..c410ba6 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2017 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.valueobject.extended + +import java.io.Serializable +import java.util.* + +/** + * A "file" attachment as used by extended encoding type messages. Could either be an attachment, + * or used inline to be used by a HTML message, for example. + */ +data class Attachment constructor( + val name: String, + val data: ByteArray, + val type: String, + val disposition: Disposition +) : Serializable { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Attachment) return false + return name == other.name && + Arrays.equals(data, other.data) && + type == other.type && + disposition == other.disposition + } + + override fun hashCode(): Int { + return Objects.hash(name, data, type, disposition) + } + + enum class Disposition { + inline, attachment + } + + class Builder { + private var name: String? = null + private var data: ByteArray? = null + private var type: String? = null + private var disposition: Disposition? = null + + fun name(name: String): Builder { + this.name = name + return this + } + + fun data(data: ByteArray): Builder { + this.data = data + return this + } + + fun type(type: String): Builder { + this.type = type + return this + } + + fun inline(): Builder { + this.disposition = Disposition.inline + return this + } + + fun attachment(): Builder { + this.disposition = Disposition.attachment + return this + } + + fun disposition(disposition: Disposition): Builder { + this.disposition = disposition + return this + } + + fun build(): Attachment { + return Attachment(name!!, data!!, type!!, disposition!!) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Message.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Message.kt new file mode 100644 index 0000000..c378394 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Message.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2017 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.valueobject.extended + +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.utils.Strings.str +import ch.dissem.msgpack.types.* +import ch.dissem.msgpack.types.Utils.mp +import org.slf4j.LoggerFactory +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.net.URLConnection +import java.nio.file.Files +import java.util.* + +/** + * Extended encoding type 'message'. Properties 'parents' and 'files' not yet supported by PyBitmessage, so they might not work + * properly with future PyBitmessage implementations. + */ +data class Message constructor( + val subject: String, + val body: String, + val parents: List<InventoryVector>, + val files: List<Attachment> +) : ExtendedEncoding.ExtendedType { + + override val type: String = TYPE + + override fun pack(): MPMap<MPString, MPType<*>> { + val result = MPMap<MPString, MPType<*>>() + result.put(mp(""), mp(TYPE)) + result.put(mp("subject"), mp(subject)) + result.put(mp("body"), mp(body)) + + if (!files.isEmpty()) { + val items = MPArray<MPMap<MPString, MPType<*>>>() + result.put(mp("files"), items) + for (file in files) { + val item = MPMap<MPString, MPType<*>>() + item.put(mp("name"), mp(file.name)) + item.put(mp("data"), mp(*file.data)) + item.put(mp("type"), mp(file.type)) + item.put(mp("disposition"), mp(file.disposition.name)) + items.add(item) + } + } + if (!parents.isEmpty()) { + val items = MPArray<MPBinary>() + result.put(mp("parents"), items) + for ((hash) in parents) { + items.add(mp(*hash)) + } + } + return result + } + + class Builder { + private var subject: String? = null + private var body: String? = null + private val parents = LinkedList<InventoryVector>() + private val files = LinkedList<Attachment>() + + fun subject(subject: String): Builder { + this.subject = subject + return this + } + + fun body(body: String): Builder { + this.body = body + return this + } + + fun addParent(parent: Plaintext?): Builder { + if (parent != null) { + val iv = parent.inventoryVector + if (iv == null) { + LOG.debug("Ignored parent without IV") + } else { + parents.add(iv) + } + } + return this + } + + fun addParent(iv: InventoryVector?): Builder { + if (iv != null) { + parents.add(iv) + } + return this + } + + fun addFile(file: File?, disposition: Attachment.Disposition): Builder { + if (file != null) { + try { + files.add(Attachment.Builder() + .name(file.name) + .disposition(disposition) + .type(URLConnection.guessContentTypeFromStream(FileInputStream(file))) + .data(Files.readAllBytes(file.toPath())) + .build()) + } catch (e: IOException) { + LOG.error(e.message, e) + } + + } + return this + } + + fun addFile(file: Attachment?): Builder { + if (file != null) { + files.add(file) + } + return this + } + + fun build(): ExtendedEncoding { + return ExtendedEncoding(Message(subject!!, body!!, parents, files)) + } + } + + class Unpacker : ExtendedEncoding.Unpacker<Message> { + override val type: String = TYPE + + override fun unpack(map: MPMap<MPString, MPType<*>>): Message { + val subject = str(map[mp("subject")]) ?: "" + val body = str(map[mp("body")]) ?: "" + val parents = LinkedList<InventoryVector>() + val files = LinkedList<Attachment>() + val mpParents = map[mp("parents")] as? MPArray<*> + for (parent in mpParents ?: emptyList<MPArray<MPBinary>>()) { + parents.add(InventoryVector.fromHash( + (parent as? MPBinary)?.value ?: continue + ) ?: continue) + } + val mpFiles = map[mp("files")] as? MPArray<*> + for (item in mpFiles ?: emptyList<Any>()) { + if (item is MPMap<*, *>) { + val b = Attachment.Builder() + b.name(str(item[mp("name")])!!) + b.data( + bin(item[mp("data")] ?: continue) ?: continue + ) + b.type(str(item[mp("type")])!!) + val disposition = str(item[mp("disposition")]) + if ("inline" == disposition) { + b.inline() + } else if ("attachment" == disposition) { + b.attachment() + } + files.add(b.build()) + } + } + + return Message(subject, body, parents, files) + } + + private fun bin(data: MPType<*>): ByteArray? { + return (data as? MPBinary)?.value + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(Message::class.java) + + val TYPE = "message" + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Vote.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Vote.kt new file mode 100644 index 0000000..d26d38a --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/extended/Vote.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2017 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.valueobject.extended + +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.utils.Strings.str +import ch.dissem.msgpack.types.MPBinary +import ch.dissem.msgpack.types.MPMap +import ch.dissem.msgpack.types.MPString +import ch.dissem.msgpack.types.MPType +import ch.dissem.msgpack.types.Utils.mp + +/** + * Extended encoding type 'vote'. Specification still outstanding, so this will need some work. + */ +data class Vote constructor(val msgId: InventoryVector, val vote: String) : ExtendedEncoding.ExtendedType { + + override val type: String = TYPE + + override fun pack(): MPMap<MPString, MPType<*>> { + val result = MPMap<MPString, MPType<*>>() + result.put(mp(""), mp(TYPE)) + result.put(mp("msgId"), mp(*msgId.hash)) + result.put(mp("vote"), mp(vote)) + return result + } + + class Builder { + private var msgId: InventoryVector? = null + private var vote: String? = null + + fun up(message: Plaintext): ExtendedEncoding { + msgId = message.inventoryVector + vote = "1" + return ExtendedEncoding(Vote(msgId!!, vote!!)) + } + + fun down(message: Plaintext): ExtendedEncoding { + msgId = message.inventoryVector + vote = "-1" + return ExtendedEncoding(Vote(msgId!!, vote!!)) + } + + fun msgId(iv: InventoryVector): Builder { + this.msgId = iv + return this + } + + fun vote(vote: String): Builder { + this.vote = vote + return this + } + + fun build(): ExtendedEncoding { + return ExtendedEncoding(Vote(msgId!!, vote!!)) + } + } + + class Unpacker : ExtendedEncoding.Unpacker<Vote> { + override val type: String + get() = TYPE + + override fun unpack(map: MPMap<MPString, MPType<*>>): Vote { + val msgId = InventoryVector.fromHash((map[mp("msgId")] as? MPBinary)?.value) ?: throw IllegalArgumentException("data doesn't contain proper msgId") + val vote = str(map[mp("vote")]) ?: throw IllegalArgumentException("no vote given") + return Vote(msgId, vote) + } + } + + companion object { + @JvmField val TYPE = "vote" + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java b/core/src/main/kotlin/ch/dissem/bitmessage/exception/AddressFormatException.kt similarity index 67% rename from core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java rename to core/src/main/kotlin/ch/dissem/bitmessage/exception/AddressFormatException.kt index 9f6674d..d838ed8 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/exception/AddressFormatException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,9 @@ * limitations under the License. */ -package ch.dissem.bitmessage.exception; +package ch.dissem.bitmessage.exception /** * Indicates an illegal Bitmessage address */ -public class AddressFormatException extends RuntimeException { - private static final long serialVersionUID = 6943764578672021573L; - - public AddressFormatException(String message) { - super(message); - } -} +class AddressFormatException(message: String) : RuntimeException(message) diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java b/core/src/main/kotlin/ch/dissem/bitmessage/exception/ApplicationException.kt similarity index 69% rename from core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java rename to core/src/main/kotlin/ch/dissem/bitmessage/exception/ApplicationException.kt index 8e49586..b520c11 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/CustomCommandHandler.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/exception/ApplicationException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,15 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; - -import ch.dissem.bitmessage.entity.CustomMessage; -import ch.dissem.bitmessage.entity.MessagePayload; +package ch.dissem.bitmessage.exception /** * @author Christian Basler */ -public interface CustomCommandHandler { - MessagePayload handle(CustomMessage request); +class ApplicationException : RuntimeException { + + constructor(cause: Throwable) : super(cause) + + constructor(message: String) : super(message) + } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/exception/DecryptionFailedException.kt b/core/src/main/kotlin/ch/dissem/bitmessage/exception/DecryptionFailedException.kt new file mode 100644 index 0000000..4d898f8 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/exception/DecryptionFailedException.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.exception + +class DecryptionFailedException : Exception() diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.kt b/core/src/main/kotlin/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.kt new file mode 100644 index 0000000..a878d88 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.exception + +import ch.dissem.bitmessage.utils.Strings +import java.io.IOException +import java.util.* + +class InsufficientProofOfWorkException(target: ByteArray, hash: ByteArray) : IOException( + "Insufficient proof of work: " + Strings.hex(target) + " required, " + + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved.") diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java b/core/src/main/kotlin/ch/dissem/bitmessage/exception/NodeException.kt similarity index 64% rename from core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java rename to core/src/main/kotlin/ch/dissem/bitmessage/exception/NodeException.kt index cecd950..0536991 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/exception/NodeException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,13 @@ * limitations under the License. */ -package ch.dissem.bitmessage.exception; +package ch.dissem.bitmessage.exception /** * An exception on the node that's severe enough to cause the client to disconnect this node. - * + * @author Ch. Basler */ -public class NodeException extends RuntimeException { - private static final long serialVersionUID = 2965325796118227802L; - - public NodeException(String message) { - super(message); - } - - public NodeException(String message, Throwable cause) { - super(message, cause); - } +class NodeException(message: String?, cause: Throwable? = null) : RuntimeException(message ?: cause?.message, cause) { + constructor(message: String) : this(message, null) } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/factory/BufferPool.kt b/core/src/main/kotlin/ch/dissem/bitmessage/factory/BufferPool.kt new file mode 100644 index 0000000..8b1d031 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/factory/BufferPool.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2017 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.factory + +import ch.dissem.bitmessage.constants.Network.HEADER_SIZE +import ch.dissem.bitmessage.constants.Network.MAX_PAYLOAD_SIZE +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import java.util.* + +/** + * A pool for [ByteBuffer]s. As they may use up a lot of memory, + * they should be reused as efficiently as possible. + */ +object BufferPool { + private val LOG = LoggerFactory.getLogger(BufferPool::class.java) + + private val pools = mapOf( + HEADER_SIZE to Stack<ByteBuffer>(), + 54 to Stack<ByteBuffer>(), + 1000 to Stack<ByteBuffer>(), + 60000 to Stack<ByteBuffer>(), + MAX_PAYLOAD_SIZE to Stack<ByteBuffer>() + ) + + @Synchronized fun allocate(capacity: Int): ByteBuffer { + val targetSize = getTargetSize(capacity) + val pool = pools[targetSize] ?: throw IllegalStateException("No pool for size $targetSize available") + if (pool.isEmpty()) { + LOG.trace("Creating new buffer of size $targetSize") + return ByteBuffer.allocate(targetSize) + } else { + return pool.pop() + } + } + + /** + * Returns a buffer that has the size of the Bitmessage network message header, 24 bytes. + + * @return a buffer of size 24 + */ + @Synchronized fun allocateHeaderBuffer(): ByteBuffer { + val pool = pools[HEADER_SIZE] + if (pool == null || pool.isEmpty()) { + return ByteBuffer.allocate(HEADER_SIZE) + } else { + return pool.pop() + } + } + + @Synchronized fun deallocate(buffer: ByteBuffer) { + buffer.clear() + val pool = pools[buffer.capacity()] ?: throw IllegalArgumentException("Illegal buffer capacity ${buffer.capacity()} one of ${pools.keys} expected.") + pool.push(buffer) + } + + private fun getTargetSize(capacity: Int): Int { + for (size in pools.keys) { + if (size >= capacity) return size + } + throw IllegalArgumentException("Requested capacity too large: requested=$capacity; max=$MAX_PAYLOAD_SIZE") + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.kt b/core/src/main/kotlin/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.kt new file mode 100644 index 0000000..d8833c7 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2017 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.factory + +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.entity.valueobject.extended.Vote +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.utils.Strings.str +import ch.dissem.msgpack.Reader +import ch.dissem.msgpack.types.MPMap +import ch.dissem.msgpack.types.MPString +import ch.dissem.msgpack.types.MPType +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.util.* +import java.util.zip.InflaterInputStream + +/** + * Factory that creates [ExtendedEncoding] objects from byte arrays. You can register your own types by adding a + * [ExtendedEncoding.Unpacker] using [.registerFactory]. + */ +object ExtendedEncodingFactory { + + private val LOG = LoggerFactory.getLogger(ExtendedEncodingFactory::class.java) + private val KEY_MESSAGE_TYPE = MPString("") + + private val factories = HashMap<String, ExtendedEncoding.Unpacker<*>>() + + init { + registerFactory(Message.Unpacker()) + registerFactory(Vote.Unpacker()) + } + + fun registerFactory(factory: ExtendedEncoding.Unpacker<*>) { + factories.put(factory.type, factory) + } + + + fun unzip(zippedData: ByteArray): ExtendedEncoding? { + try { + InflaterInputStream(ByteArrayInputStream(zippedData)).use { unzipper -> + val reader = Reader.getInstance() + @Suppress("UNCHECKED_CAST") + val map = reader.read(unzipper) as MPMap<MPString, MPType<*>> + val messageType = map[KEY_MESSAGE_TYPE] + if (messageType == null) { + LOG.error("Missing message type") + return null + } + val factory = factories[str(messageType)] + return ExtendedEncoding( + factory?.unpack(map) ?: return null + ) + } + } catch (e: ClassCastException) { + throw ApplicationException(e) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/factory/Factory.kt b/core/src/main/kotlin/ch/dissem/bitmessage/factory/Factory.kt new file mode 100644 index 0000000..9780c4b --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/factory/Factory.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2017 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.factory + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.NetworkMessage +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.payload.* +import ch.dissem.bitmessage.entity.payload.ObjectType.* +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.exception.NodeException +import ch.dissem.bitmessage.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.UnixTime +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.InputStream +import java.net.SocketException +import java.net.SocketTimeoutException + +/** + * Creates [NetworkMessage] objects from [InputStreams][InputStream] + */ +object Factory { + private val LOG = LoggerFactory.getLogger(Factory::class.java) + + @Throws(SocketTimeoutException::class) + @JvmStatic fun getNetworkMessage(@Suppress("UNUSED_PARAMETER") version: Int, stream: InputStream): NetworkMessage? { + try { + return V3MessageFactory.read(stream) + } catch (e: Exception) { + when (e) { + is SocketTimeoutException, + is NodeException -> throw e + is SocketException -> throw NodeException(e.message, e) + else -> { + LOG.error(e.message, e) + return null + } + } + } + + } + + @JvmStatic fun getObjectMessage(@Suppress("UNUSED_PARAMETER") version: Int, stream: InputStream, length: Int): ObjectMessage? { + try { + return V3MessageFactory.readObject(stream, length) + } catch (e: IOException) { + LOG.error(e.message, e) + return null + } + } + + @JvmStatic fun createPubkey(version: Long, stream: Long, publicSigningKey: ByteArray, publicEncryptionKey: ByteArray, + nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature): Pubkey { + return createPubkey(version, stream, publicSigningKey, publicEncryptionKey, nonceTrialsPerByte, extraBytes, + Pubkey.Feature.bitfield(*features)) + } + + @JvmStatic fun createPubkey(version: Long, stream: Long, publicSigningKey: ByteArray, publicEncryptionKey: ByteArray, + nonceTrialsPerByte: Long, extraBytes: Long, behaviourBitfield: Int): Pubkey { + if (publicSigningKey.size != 64 && publicSigningKey.size != 65) + throw IllegalArgumentException("64 bytes signing key expected, but it was " + + publicSigningKey.size + " bytes long.") + if (publicEncryptionKey.size != 64 && publicEncryptionKey.size != 65) + throw IllegalArgumentException("64 bytes encryption key expected, but it was " + + publicEncryptionKey.size + " bytes long.") + + when (version.toInt()) { + 2 -> return V2Pubkey.Builder() + .stream(stream) + .publicSigningKey(publicSigningKey) + .publicEncryptionKey(publicEncryptionKey) + .behaviorBitfield(behaviourBitfield) + .build() + 3 -> return V3Pubkey.Builder() + .stream(stream) + .publicSigningKey(publicSigningKey) + .publicEncryptionKey(publicEncryptionKey) + .behaviorBitfield(behaviourBitfield) + .nonceTrialsPerByte(nonceTrialsPerByte) + .extraBytes(extraBytes) + .build() + 4 -> return V4Pubkey( + V3Pubkey.Builder() + .stream(stream) + .publicSigningKey(publicSigningKey) + .publicEncryptionKey(publicEncryptionKey) + .behaviorBitfield(behaviourBitfield) + .nonceTrialsPerByte(nonceTrialsPerByte) + .extraBytes(extraBytes) + .build() + ) + else -> throw IllegalArgumentException("Unexpected pubkey version " + version) + } + } + + @JvmStatic fun createIdentityFromPrivateKey(address: String, + privateSigningKey: ByteArray, privateEncryptionKey: ByteArray, + nonceTrialsPerByte: Long, extraBytes: Long, + behaviourBitfield: Int): BitmessageAddress { + val temp = BitmessageAddress(address) + val privateKey = PrivateKey(privateSigningKey, privateEncryptionKey, + createPubkey(temp.version, temp.stream, + cryptography().createPublicKey(privateSigningKey), + cryptography().createPublicKey(privateEncryptionKey), + nonceTrialsPerByte, extraBytes, behaviourBitfield)) + val result = BitmessageAddress(privateKey) + if (result.address != address) { + throw IllegalArgumentException("Address not matching private key. Address: " + address + + "; Address derived from private key: " + result.address) + } + return result + } + + @JvmStatic fun generatePrivateAddress(shorter: Boolean, + stream: Long, + vararg features: Pubkey.Feature): BitmessageAddress { + return BitmessageAddress(PrivateKey(shorter, stream, 1000, 1000, *features)) + } + + @JvmStatic fun getObjectPayload(objectType: Long, + version: Long, + streamNumber: Long, + stream: InputStream, + length: Int): ObjectPayload { + val type = ObjectType.fromNumber(objectType) + if (type != null) { + when (type) { + GET_PUBKEY -> return parseGetPubkey(version, streamNumber, stream, length) + PUBKEY -> return parsePubkey(version, streamNumber, stream, length) + MSG -> return parseMsg(version, streamNumber, stream, length) + BROADCAST -> return parseBroadcast(version, streamNumber, stream, length) + } + } + // fallback: just store the message - we don't really care what it is + LOG.trace("Unexpected object type: " + objectType) + return GenericPayload.read(version, streamNumber, stream, length) + } + + @JvmStatic private fun parseGetPubkey(version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload { + return GetPubkey.read(stream, streamNumber, length, version) + } + + @JvmStatic fun readPubkey(version: Long, stream: Long, `is`: InputStream, length: Int, encrypted: Boolean): Pubkey? { + when (version.toInt()) { + 2 -> return V2Pubkey.read(`is`, stream) + 3 -> return V3Pubkey.read(`is`, stream) + 4 -> return V4Pubkey.read(`is`, stream, length, encrypted) + } + LOG.debug("Unexpected pubkey version $version, handling as generic payload object") + return null + } + + @JvmStatic private fun parsePubkey(version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload { + val pubkey = readPubkey(version, streamNumber, stream, length, true) + return pubkey ?: GenericPayload.read(version, streamNumber, stream, length) + } + + @JvmStatic private fun parseMsg(@Suppress("UNUSED_PARAMETER") version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload { + return Msg.read(stream, streamNumber, length) + } + + @JvmStatic private fun parseBroadcast(version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload { + when (version.toInt()) { + 4 -> return V4Broadcast.read(stream, streamNumber, length) + 5 -> return V5Broadcast.read(stream, streamNumber, length) + else -> { + LOG.debug("Encountered unknown broadcast version " + version) + return GenericPayload.read(version, streamNumber, stream, length) + } + } + } + + @JvmStatic fun getBroadcast(plaintext: Plaintext): Broadcast { + val sendingAddress = plaintext.from + if (sendingAddress.version < 4) { + return V4Broadcast(sendingAddress, plaintext) + } else { + return V5Broadcast(sendingAddress, plaintext) + } + } + + @JvmStatic fun createAck(from: BitmessageAddress, ackData: ByteArray?, ttl: Long): ObjectMessage? { + val ack = GenericPayload( + 3, from.stream, + ackData ?: return null + ) + return ObjectMessage.Builder().objectType(MSG).payload(ack).expiresTime(UnixTime.now + ttl).build() + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/factory/V3MessageFactory.kt b/core/src/main/kotlin/ch/dissem/bitmessage/factory/V3MessageFactory.kt new file mode 100644 index 0000000..57fc49f --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/factory/V3MessageFactory.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2017 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.factory + +import ch.dissem.bitmessage.entity.* +import ch.dissem.bitmessage.entity.payload.GenericPayload +import ch.dissem.bitmessage.entity.payload.ObjectPayload +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +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.Singleton.cryptography +import ch.dissem.bitmessage.utils.Strings +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.util.* + +/** + * Creates protocol v3 network messages from [InputStreams][InputStream] + */ +object V3MessageFactory { + private val LOG = LoggerFactory.getLogger(V3MessageFactory::class.java) + + @JvmStatic + fun read(`in`: InputStream): NetworkMessage? { + findMagic(`in`) + val command = getCommand(`in`) + val length = Decode.uint32(`in`).toInt() + if (length > 1600003) { + throw NodeException("Payload of $length bytes received, no more than 1600003 was expected.") + } + val checksum = Decode.bytes(`in`, 4) + + val payloadBytes = Decode.bytes(`in`, length) + + if (testChecksum(checksum, payloadBytes)) { + val payload = getPayload(command, ByteArrayInputStream(payloadBytes), length) + if (payload != null) + return NetworkMessage(payload) + else + return null + } else { + throw IOException("Checksum failed for message '$command'") + } + } + + @JvmStatic + fun getPayload(command: String, stream: InputStream, length: Int): MessagePayload? { + when (command) { + "version" -> return parseVersion(stream) + "verack" -> return VerAck() + "addr" -> return parseAddr(stream) + "inv" -> return parseInv(stream) + "getdata" -> return parseGetData(stream) + "object" -> return readObject(stream, length) + "custom" -> return readCustom(stream, length) + else -> { + LOG.debug("Unknown command: " + command) + return null + } + } + } + + private fun readCustom(`in`: InputStream, length: Int): MessagePayload { + return CustomMessage.read(`in`, length) + } + + @JvmStatic + fun readObject(`in`: InputStream, length: Int): ObjectMessage { + val counter = AccessCounter() + val nonce = Decode.bytes(`in`, 8, counter) + val expiresTime = Decode.int64(`in`, counter) + val objectType = Decode.uint32(`in`, counter) + val version = Decode.varInt(`in`, counter) + val stream = Decode.varInt(`in`, counter) + + val data = Decode.bytes(`in`, length - counter.length()) + var payload: ObjectPayload + try { + val dataStream = ByteArrayInputStream(data) + payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.size) + } catch (e: Exception) { + if (LOG.isTraceEnabled) { + LOG.trace("Could not parse object payload - using generic payload instead", e) + LOG.trace(Strings.hex(data)) + } + payload = GenericPayload(version, stream, data) + } + + return ObjectMessage.Builder() + .nonce(nonce) + .expiresTime(expiresTime) + .objectType(objectType) + .stream(stream) + .payload(payload) + .build() + } + + private fun parseGetData(stream: InputStream): GetData { + val count = Decode.varInt(stream) + val inventoryVectors = LinkedList<InventoryVector>() + for (i in 0..count - 1) { + inventoryVectors.add(parseInventoryVector(stream)) + } + return GetData(inventoryVectors) + } + + private fun parseInv(stream: InputStream): Inv { + val count = Decode.varInt(stream) + val inventoryVectors = LinkedList<InventoryVector>() + for (i in 0..count - 1) { + inventoryVectors.add(parseInventoryVector(stream)) + } + return Inv(inventoryVectors) + } + + private fun parseAddr(stream: InputStream): Addr { + val count = Decode.varInt(stream) + val networkAddresses = LinkedList<NetworkAddress>() + for (i in 0..count - 1) { + networkAddresses.add(parseAddress(stream, false)) + } + return Addr(networkAddresses) + } + + private fun parseVersion(stream: InputStream): Version { + val version = Decode.int32(stream) + val services = Decode.int64(stream) + val timestamp = Decode.int64(stream) + val addrRecv = parseAddress(stream, true) + val addrFrom = parseAddress(stream, true) + val nonce = Decode.int64(stream) + val userAgent = Decode.varString(stream) + val streamNumbers = Decode.varIntList(stream) + + return Version.Builder() + .version(version) + .services(services) + .timestamp(timestamp) + .addrRecv(addrRecv).addrFrom(addrFrom) + .nonce(nonce) + .userAgent(userAgent) + .streams(*streamNumbers).build() + } + + private fun parseInventoryVector(stream: InputStream): InventoryVector { + return InventoryVector(Decode.bytes(stream, 32)) + } + + private fun parseAddress(stream: InputStream, light: Boolean): NetworkAddress { + val time: Long + val streamNumber: Long + if (!light) { + time = Decode.int64(stream) + streamNumber = Decode.uint32(stream) // This isn't consistent, not sure if this is correct + } else { + time = 0 + streamNumber = 0 + } + val services = Decode.int64(stream) + val ipv6 = Decode.bytes(stream, 16) + val port = Decode.uint16(stream) + return NetworkAddress.Builder() + .time(time) + .stream(streamNumber) + .services(services) + .ipv6(ipv6) + .port(port) + .build() + } + + private fun testChecksum(checksum: ByteArray, payload: ByteArray): Boolean { + val payloadChecksum = cryptography().sha512(payload) + for (i in checksum.indices) { + if (checksum[i] != payloadChecksum[i]) { + return false + } + } + return true + } + + private fun getCommand(stream: InputStream): String { + val bytes = ByteArray(12) + var end = bytes.size + for (i in bytes.indices) { + bytes[i] = stream.read().toByte() + if (end == bytes.size) { + if (bytes[i].toInt() == 0) end = i + } else { + if (bytes[i].toInt() != 0) throw IOException("'\\u0000' padding expected for command") + } + } + return String(bytes, 0, end, Charsets.US_ASCII) + } + + private fun findMagic(`in`: InputStream) { + var pos = 0 + for (i in 0..1619999) { + val b = `in`.read().toByte() + if (b == NetworkMessage.MAGIC_BYTES[pos]) { + if (pos + 1 == NetworkMessage.MAGIC_BYTES.size) { + return + } + } else if (pos > 0 && b == NetworkMessage.MAGIC_BYTES[0]) { + pos = 1 + } else { + pos = 0 + } + pos++ + } + throw NodeException("Failed to find MAGIC bytes in stream") + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/factory/V3MessageReader.kt b/core/src/main/kotlin/ch/dissem/bitmessage/factory/V3MessageReader.kt new file mode 100644 index 0000000..5b81621 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/factory/V3MessageReader.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2017 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.factory + +import ch.dissem.bitmessage.constants.Network.MAX_PAYLOAD_SIZE +import ch.dissem.bitmessage.entity.NetworkMessage +import ch.dissem.bitmessage.exception.NodeException +import ch.dissem.bitmessage.utils.Decode +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.ByteArrayInputStream +import java.io.IOException +import java.nio.ByteBuffer +import java.util.* + +/** + * Similar to the [V3MessageFactory], but used for NIO buffers which may or may not contain a whole message. + */ +class V3MessageReader { + private var headerBuffer: ByteBuffer? = null + private var dataBuffer: ByteBuffer? = null + + private var state: ReaderState? = ReaderState.MAGIC + private var command: String? = null + private var length: Int = 0 + private val checksum = ByteArray(4) + + private val messages = LinkedList<NetworkMessage>() + + fun getActiveBuffer(): ByteBuffer { + if (state != null && state != ReaderState.DATA) { + if (headerBuffer == null) { + headerBuffer = BufferPool.allocateHeaderBuffer() + } + } + return if (state == ReaderState.DATA) + dataBuffer ?: throw IllegalStateException("data buffer is null") + else + headerBuffer ?: throw IllegalStateException("header buffer is null") + } + + fun update() { + if (state != ReaderState.DATA) { + getActiveBuffer() // in order to initialize + headerBuffer?.flip() ?: throw IllegalStateException("header buffer is null") + } + when (state) { + V3MessageReader.ReaderState.MAGIC -> magic(headerBuffer ?: throw IllegalStateException("header buffer is null")) + V3MessageReader.ReaderState.HEADER -> header(headerBuffer ?: throw IllegalStateException("header buffer is null")) + V3MessageReader.ReaderState.DATA -> data(dataBuffer ?: throw IllegalStateException("data buffer is null")) + } + } + + private fun magic(headerBuffer: ByteBuffer) { + if (!findMagicBytes(headerBuffer)) { + headerBuffer.compact() + return + } else { + state = ReaderState.HEADER + header(headerBuffer) + } + } + + private fun header(headerBuffer: ByteBuffer) { + if (headerBuffer.remaining() < 20) { + headerBuffer.compact() + headerBuffer.limit(20) + return + } + command = getCommand(headerBuffer) + length = Decode.uint32(headerBuffer).toInt() + if (length > MAX_PAYLOAD_SIZE) { + throw NodeException("Payload of " + length + " bytes received, no more than " + + MAX_PAYLOAD_SIZE + " was expected.") + } + headerBuffer.get(checksum) + state = ReaderState.DATA + this.headerBuffer = null + BufferPool.deallocate(headerBuffer) + val dataBuffer = BufferPool.allocate(length) + this.dataBuffer = dataBuffer + dataBuffer.clear() + dataBuffer.limit(length) + data(dataBuffer) + } + + private fun data(dataBuffer: ByteBuffer) { + if (dataBuffer.position() < length) { + return + } else { + dataBuffer.flip() + } + if (!testChecksum(dataBuffer)) { + state = ReaderState.MAGIC + this.dataBuffer = null + BufferPool.deallocate(dataBuffer) + throw NodeException("Checksum failed for message '$command'") + } + try { + V3MessageFactory.getPayload( + command ?: throw IllegalStateException("command is null"), + ByteArrayInputStream(dataBuffer.array(), + dataBuffer.arrayOffset() + dataBuffer.position(), length), + length + )?.let { messages.add(NetworkMessage(it)) } + } catch (e: IOException) { + throw NodeException(e.message) + } finally { + state = ReaderState.MAGIC + this.dataBuffer = null + BufferPool.deallocate(dataBuffer) + } + } + + fun getMessages(): MutableList<NetworkMessage> { + return messages + } + + private fun findMagicBytes(buffer: ByteBuffer): Boolean { + var i = 0 + while (buffer.hasRemaining()) { + if (i == 0) { + buffer.mark() + } + if (buffer.get() == NetworkMessage.MAGIC_BYTES[i]) { + i++ + if (i == NetworkMessage.MAGIC_BYTES.size) { + return true + } + } else { + i = 0 + } + } + if (i > 0) { + buffer.reset() + } + return false + } + + private fun getCommand(buffer: ByteBuffer): String { + val start = buffer.position() + var l = 0 + while (l < 12 && buffer.get().toInt() != 0) l++ + var i = l + 1 + while (i < 12) { + if (buffer.get().toInt() != 0) throw NodeException("'\\u0000' padding expected for command") + i++ + } + return String(buffer.array(), start, l, Charsets.US_ASCII) + } + + private fun testChecksum(buffer: ByteBuffer): Boolean { + val payloadChecksum = cryptography().sha512(buffer.array(), + buffer.arrayOffset() + buffer.position(), length) + for (i in checksum.indices) { + if (checksum[i] != payloadChecksum[i]) { + return false + } + } + return true + } + + /** + * De-allocates all buffers. This method should be called iff the reader isn't used anymore, i.e. when its + * connection is severed. + */ + fun cleanup() { + state = null + headerBuffer?.let { BufferPool.deallocate(it) } + dataBuffer?.let { BufferPool.deallocate(it) } + } + + private enum class ReaderState { + MAGIC, HEADER, DATA + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt new file mode 100644 index 0000000..f6b779b --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2017 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.InternalContext.Companion.NETWORK_EXTRA_BYTES +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.Bytes +import ch.dissem.bitmessage.utils.UnixTime +import ch.dissem.bitmessage.utils.max +import org.slf4j.LoggerFactory +import java.math.BigInteger +import java.security.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * Implements everything that isn't directly dependent on either Spongy- or Bouncycastle. + */ +abstract class AbstractCryptography protected constructor(@JvmField protected val provider: Provider) : Cryptography, InternalContext.ContextHolder { + private lateinit var ctx: InternalContext + + @JvmField protected val ALGORITHM_ECDSA = "ECDSA" + @JvmField protected val ALGORITHM_ECDSA_SHA1 = "SHA1withECDSA" + @JvmField protected val ALGORITHM_EVP_SHA256 = "SHA256withECDSA" + + override fun setContext(context: InternalContext) { + ctx = context + } + + override fun sha512(data: ByteArray, offset: Int, length: Int): ByteArray { + val mda = md("SHA-512") + mda.update(data, offset, length) + return mda.digest() + } + + override fun sha512(vararg data: ByteArray): ByteArray { + return hash("SHA-512", *data) + } + + override fun doubleSha512(vararg data: ByteArray): ByteArray { + val mda = md("SHA-512") + for (d in data) { + mda.update(d) + } + return mda.digest(mda.digest()) + } + + override fun doubleSha512(data: ByteArray, length: Int): ByteArray { + val mda = md("SHA-512") + mda.update(data, 0, length) + return mda.digest(mda.digest()) + } + + override fun ripemd160(vararg data: ByteArray): ByteArray { + return hash("RIPEMD160", *data) + } + + override fun doubleSha256(data: ByteArray, length: Int): ByteArray { + val mda = md("SHA-256") + mda.update(data, 0, length) + return mda.digest(mda.digest()) + } + + override fun sha1(vararg data: ByteArray): ByteArray { + return hash("SHA-1", *data) + } + + override fun randomBytes(length: Int): ByteArray { + val result = ByteArray(length) + RANDOM.nextBytes(result) + return result + } + + override fun doProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, + extraBytes: Long, callback: ProofOfWorkEngine.Callback) { + + val initialHash = getInitialHash(objectMessage) + + val target = getProofOfWorkTarget(objectMessage, + max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE), max(extraBytes, NETWORK_EXTRA_BYTES)) + + ctx.proofOfWorkEngine.calculateNonce(initialHash, target, callback) + } + + @Throws(InsufficientProofOfWorkException::class) + override fun checkProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) { + val target = getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes) + val value = doubleSha512(objectMessage.nonce ?: throw ApplicationException("Object without nonce"), getInitialHash(objectMessage)) + if (Bytes.lt(target, value, 8)) { + throw InsufficientProofOfWorkException(target, value) + } + } + + protected fun doSign(data: ByteArray, privKey: java.security.PrivateKey): ByteArray { + // TODO: change this to ALGORITHM_EVP_SHA256 once it's generally used in the network + val sig = Signature.getInstance(ALGORITHM_ECDSA_SHA1, provider) + sig.initSign(privKey) + sig.update(data) + return sig.sign() + } + + + protected fun doCheckSignature(data: ByteArray, signature: ByteArray, publicKey: PublicKey): Boolean { + for (algorithm in arrayOf(ALGORITHM_ECDSA_SHA1, ALGORITHM_EVP_SHA256)) { + val sig = Signature.getInstance(algorithm, provider) + sig.initVerify(publicKey) + sig.update(data) + if (sig.verify(signature)) { + return true + } + } + return false + } + + override fun getInitialHash(objectMessage: ObjectMessage): ByteArray { + return sha512(objectMessage.payloadBytesWithoutNonce) + } + + override fun getProofOfWorkTarget(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long): ByteArray { + @Suppress("NAME_SHADOWING") + val nonceTrialsPerByte = if (nonceTrialsPerByte == 0L) NETWORK_NONCE_TRIALS_PER_BYTE else nonceTrialsPerByte + @Suppress("NAME_SHADOWING") + val extraBytes = if (extraBytes == 0L) NETWORK_EXTRA_BYTES else extraBytes + + val TTL = BigInteger.valueOf(objectMessage.expiresTime - UnixTime.now) + val powLength = BigInteger.valueOf(objectMessage.payloadBytesWithoutNonce.size + extraBytes) + val denominator = BigInteger.valueOf(nonceTrialsPerByte) + .multiply( + powLength.add( + powLength.multiply(TTL).divide(TWO_POW_16) + ) + ) + return Bytes.expand(TWO_POW_64.divide(denominator).toByteArray(), 8) + } + + private fun hash(algorithm: String, vararg data: ByteArray): ByteArray { + val mda = md(algorithm) + for (d in data) { + mda.update(d) + } + return mda.digest() + } + + private fun md(algorithm: String): MessageDigest { + try { + return MessageDigest.getInstance(algorithm, provider) + } catch (e: GeneralSecurityException) { + throw ApplicationException(e) + } + + } + + override fun mac(key_m: ByteArray, data: ByteArray): ByteArray { + try { + val mac = Mac.getInstance("HmacSHA256", provider) + mac.init(SecretKeySpec(key_m, "HmacSHA256")) + return mac.doFinal(data) + } catch (e: GeneralSecurityException) { + throw ApplicationException(e) + } + + } + + override fun createPubkey(version: Long, stream: Long, privateSigningKey: ByteArray, privateEncryptionKey: ByteArray, + nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature): Pubkey { + return Factory.createPubkey(version, stream, + createPublicKey(privateSigningKey), + createPublicKey(privateEncryptionKey), + nonceTrialsPerByte, extraBytes, *features) + } + + override fun keyToBigInt(privateKey: ByteArray): BigInteger { + return BigInteger(1, privateKey) + } + + override fun randomNonce(): Long { + return RANDOM.nextLong() + } + + companion object { + protected val LOG = LoggerFactory.getLogger(Cryptography::class.java) + private val RANDOM = SecureRandom() + private val TWO = BigInteger.valueOf(2) + private val TWO_POW_64 = TWO.pow(64) + private val TWO_POW_16 = TWO.pow(16) + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt new file mode 100644 index 0000000..b6d2108 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports + +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.utils.SqlStrings.join +import ch.dissem.bitmessage.utils.Strings +import ch.dissem.bitmessage.utils.UnixTime +import java.util.* + +abstract class AbstractMessageRepository : MessageRepository, InternalContext.ContextHolder { + protected lateinit var ctx: InternalContext + + override fun setContext(context: InternalContext) { + ctx = context + } + + protected fun saveContactIfNecessary(contact: BitmessageAddress?) { + contact?.let { + val savedAddress = ctx.addressRepository.getAddress(it.address) + if (savedAddress == null) { + ctx.addressRepository.save(it) + } else { + if (savedAddress.pubkey == null && it.pubkey != null) { + savedAddress.pubkey = it.pubkey + ctx.addressRepository.save(savedAddress) + } + it.alias = savedAddress.alias + } + } + } + + override fun getMessage(id: Any): Plaintext { + if (id is Long) { + return single(find("id=" + id)) ?: throw IllegalArgumentException("There is no message with id $id") + } else { + throw IllegalArgumentException("Long expected for ID") + } + } + + override fun getMessage(iv: InventoryVector): Plaintext? { + return single(find("iv=X'" + Strings.hex(iv.hash) + "'")) + } + + override fun getMessage(initialHash: ByteArray): Plaintext? { + return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'")) + } + + override fun getMessageForAck(ackData: ByteArray): Plaintext? { + return single(find("ack_data=X'" + Strings.hex(ackData) + "' AND status='" + Plaintext.Status.SENT + "'")) + } + + override fun findMessages(label: Label?): List<Plaintext> { + if (label == null) { + return find("id NOT IN (SELECT message_id FROM Message_Label)") + } else { + return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")") + } + } + + override fun findMessages(status: Plaintext.Status, recipient: BitmessageAddress): List<Plaintext> { + return find("status='" + status.name + "' AND recipient='" + recipient.address + "'") + } + + override fun findMessages(status: Plaintext.Status): List<Plaintext> { + return find("status='" + status.name + "'") + } + + override fun findMessages(sender: BitmessageAddress): List<Plaintext> { + return find("sender='" + sender.address + "'") + } + + override fun findMessagesToResend(): List<Plaintext> { + return find("status='" + Plaintext.Status.SENT.name + "'" + + " AND next_try < " + UnixTime.now) + } + + override fun findResponses(parent: Plaintext): List<Plaintext> { + if (parent.inventoryVector == null) { + return emptyList() + } + return find("iv IN (SELECT child FROM Message_Parent" + + " WHERE parent=X'" + Strings.hex(parent.inventoryVector!!.hash) + "')") + } + + override fun getConversation(conversationId: UUID): List<Plaintext> { + return find("conversation=X'" + conversationId.toString().replace("-", "") + "'") + } + + override fun getLabels(): List<Label> { + return findLabels("1=1") + } + + override fun getLabels(vararg types: Label.Type): List<Label> { + return findLabels("type IN (" + join(*types) + ")") + } + + protected abstract fun findLabels(where: String): List<Label> + + + protected fun <T> single(collection: Collection<T>): T? { + when (collection.size) { + 0 -> return null + 1 -> return collection.iterator().next() + else -> throw ApplicationException("This shouldn't happen, found " + collection.size + + " items, one or none was expected") + } + } + + protected abstract fun find(where: String): List<Plaintext> +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AddressRepository.kt similarity index 63% rename from core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/AddressRepository.kt index 5247730..811785a 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AddressRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,52 +14,51 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; +package ch.dissem.bitmessage.ports -import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.BitmessageAddress -import java.util.List; - -public interface AddressRepository { +interface AddressRepository { /** * Returns a matching BitmessageAddress if there is one with the given ripe or tag, that * has no public key yet. If it doesn't exist or already has a public key, null is returned. - * + * @param ripeOrTag Either ripe or tag (depending of address version) of an address with - * missing public key. + * * missing public key. + * * * @return the matching address if there is one without public key, or null otherwise. */ - BitmessageAddress findContact(byte[] ripeOrTag); + fun findContact(ripeOrTag: ByteArray): BitmessageAddress? - BitmessageAddress findIdentity(byte[] ripeOrTag); + fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? /** * @return all Bitmessage addresses that belong to this user, i.e. have a private key. */ - List<BitmessageAddress> getIdentities(); + fun getIdentities(): List<BitmessageAddress> /** * @return all subscribed chans. */ - List<BitmessageAddress> getChans(); + fun getChans(): List<BitmessageAddress> - List<BitmessageAddress> getSubscriptions(); + fun getSubscriptions(): List<BitmessageAddress> - List<BitmessageAddress> getSubscriptions(long broadcastVersion); + fun getSubscriptions(broadcastVersion: Long): List<BitmessageAddress> /** * @return all Bitmessage addresses that have no private key or are chans. */ - List<BitmessageAddress> getContacts(); + fun getContacts(): List<BitmessageAddress> /** - * Implementations must not delete cryptographic keys if they're not provided by <code>address</code>. - * + * Implementations must not delete cryptographic keys if they're not provided by `address`. + * @param address to save or update */ - void save(BitmessageAddress address); + fun save(address: BitmessageAddress) - void remove(BitmessageAddress address); + fun remove(address: BitmessageAddress) - BitmessageAddress getAddress(String address); + fun getAddress(address: String): BitmessageAddress? } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/Cryptography.kt similarity index 64% rename from core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/Cryptography.kt index 9ea6a9d..32f7dee 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/Cryptography.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -11,212 +11,253 @@ * 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. + * limitations under the License. */ -package ch.dissem.bitmessage.ports; +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; +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException +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}, + * Provides some methods to help with hashing and encryption. All randoms are created using [SecureRandom], * which should be secure enough. */ -public interface Cryptography { +interface Cryptography { /** - * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at + * A helper method to calculate SHA-512 hashes. Please note that a new [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 + * * * @param offset of the data to be hashed + * * * @param length of the data to be hashed + * * * @return SHA-512 hash of data within the given range */ - byte[] sha512(byte[] data, int offset, int length); + fun sha512(data: ByteArray, offset: Int, length: Int): ByteArray /** - * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at + * A helper method to calculate SHA-512 hashes. Please note that a new [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); + fun sha512(vararg data: ByteArray): ByteArray /** - * A helper method to calculate doubleSHA-512 hashes. Please note that a new {@link MessageDigest} object is created + * A helper method to calculate doubleSHA-512 hashes. Please note that a new [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); + fun doubleSha512(vararg data: ByteArray): ByteArray /** * 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> * + * + * Please note that a new [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. + * + * @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); + fun doubleSha512(data: ByteArray, length: Int): ByteArray /** * 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 + * + * + * Please note that a new [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); + fun ripemd160(vararg data: ByteArray): ByteArray /** * 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 + * + * + * Please note that a new [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); + fun doubleSha256(data: ByteArray, length: Int): ByteArray /** * 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 + * + * + * Please note that a new [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); + fun sha1(vararg data: ByteArray): ByteArray /** * @param length number of bytes to return + * * * @return an array of the given size containing random bytes */ - byte[] randomBytes(int length); + fun randomBytes(length: Int): ByteArray /** * 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 objectMessage 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); + fun doProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, + extraBytes: Long, callback: ProofOfWorkEngine.Callback) /** - * @param object to be checked + * @param objectMessage 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; + @Throws(InsufficientProofOfWorkException::class) + fun checkProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) - byte[] getInitialHash(ObjectMessage object); + fun getInitialHash(objectMessage: ObjectMessage): ByteArray - byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes); + fun getProofOfWorkTarget(objectMessage: ObjectMessage, nonceTrialsPerByte: Long = NETWORK_NONCE_TRIALS_PER_BYTE, extraBytes: Long = NETWORK_EXTRA_BYTES): ByteArray /** * 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); + fun mac(key_m: ByteArray, data: ByteArray): ByteArray /** * @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); + fun crypt(encrypt: Boolean, data: ByteArray, key_e: ByteArray, initializationVector: ByteArray): ByteArray /** * 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); + fun createPubkey(version: Long, stream: Long, privateSigningKey: ByteArray, privateEncryptionKey: ByteArray, + nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature): Pubkey /** * @param privateKey private key as byte array + * * * @return a public key corresponding to the given private key */ - byte[] createPublicKey(byte[] privateKey); + fun createPublicKey(privateKey: ByteArray): ByteArray /** * @param privateKey private key as byte array + * * * @return a big integer representation (unsigned) of the given bytes */ - BigInteger keyToBigInt(byte[] privateKey); + fun keyToBigInt(privateKey: ByteArray): BigInteger /** * @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); + fun isSignatureValid(data: ByteArray, signature: ByteArray, pubkey: Pubkey): Boolean /** * 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); + fun getSignature(data: ByteArray, privateKey: ch.dissem.bitmessage.entity.valueobject.PrivateKey): ByteArray /** * @return a random number of type long */ - long randomNonce(); + fun randomNonce(): Long - byte[] multiply(byte[] k, byte[] r); + fun multiply(k: ByteArray, r: ByteArray): ByteArray - byte[] createPoint(byte[] x, byte[] y); + fun createPoint(x: ByteArray, y: ByteArray): ByteArray } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/CustomCommandHandler.kt similarity index 66% rename from core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/CustomCommandHandler.kt index 4c73fe3..3f8b63b 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/CustomCommandHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,14 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.ports -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; -import org.junit.BeforeClass; +import ch.dissem.bitmessage.entity.CustomMessage +import ch.dissem.bitmessage.entity.MessagePayload /** * @author Christian Basler */ -public class TestBase { - @BeforeClass - public static void setUpClass() { - Singleton.initialize(new BouncyCryptography()); - } +interface CustomCommandHandler { + fun handle(request: CustomMessage): MessagePayload? } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/DefaultLabeler.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/DefaultLabeler.kt new file mode 100644 index 0000000..cc55db9 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/DefaultLabeler.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports + +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Status.* +import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST +import ch.dissem.bitmessage.entity.valueobject.Label + +open class DefaultLabeler : Labeler, InternalContext.ContextHolder { + private lateinit var ctx: InternalContext + + override fun setContext(context: InternalContext) { + ctx = context + } + + override fun setLabels(msg: Plaintext) { + msg.status = RECEIVED + if (msg.type == BROADCAST) { + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)) + } else { + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.INBOX, Label.Type.UNREAD)) + } + } + + override fun markAsDraft(msg: Plaintext) { + msg.status = DRAFT + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.DRAFT)) + } + + override fun markAsSending(msg: Plaintext) { + if (msg.to != null && msg.to!!.pubkey == null) { + msg.status = PUBKEY_REQUESTED + } else { + msg.status = DOING_PROOF_OF_WORK + } + msg.removeLabel(Label.Type.DRAFT) + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.OUTBOX)) + } + + override fun markAsSent(msg: Plaintext) { + msg.status = SENT + msg.removeLabel(Label.Type.OUTBOX) + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.SENT)) + } + + override fun markAsAcknowledged(msg: Plaintext) { + msg.status = SENT_ACKNOWLEDGED + } + + override fun markAsRead(msg: Plaintext) { + msg.removeLabel(Label.Type.UNREAD) + } + + override fun markAsUnread(msg: Plaintext) { + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.UNREAD)) + } + + override fun delete(msg: Plaintext) { + msg.labels.clear() + msg.addLabels(ctx.messageRepository.getLabels(Label.Type.TRASH)) + } + + override fun archive(msg: Plaintext) { + msg.labels.clear() + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/Inventory.kt similarity index 64% rename from core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/Inventory.kt index 6af65c1..9c4885d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/Inventory.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/Inventory.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,44 +14,42 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; +package ch.dissem.bitmessage.ports -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; - -import java.util.List; +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.entity.valueobject.InventoryVector /** * The Inventory stores and retrieves objects, cleans up outdated objects and can tell which objects are still missing. */ -public interface Inventory { +interface Inventory { /** * Returns the IVs of all valid objects we have for the given streams */ - List<InventoryVector> getInventory(long... streams); + fun getInventory(vararg streams: Long): List<InventoryVector> /** * 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); + fun getMissing(offer: List<InventoryVector>, vararg streams: Long): List<InventoryVector> - ObjectMessage getObject(InventoryVector vector); + fun getObject(vector: InventoryVector): ObjectMessage? /** * 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); + fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> - void storeObject(ObjectMessage object); + fun storeObject(objectMessage: ObjectMessage) - boolean contains(ObjectMessage object); + operator fun contains(objectMessage: ObjectMessage): Boolean /** * Deletes all objects that expired 5 minutes ago or earlier * (so we don't accidentally request objects we just deleted) */ - void cleanup(); + fun cleanup() } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/Labeler.kt similarity index 56% rename from core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/Labeler.kt index e79bee2..35ddf16 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/Labeler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,44 +14,43 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; +package ch.dissem.bitmessage.ports -import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.Plaintext /** * Defines and sets labels. Note that it should also update the status field of a message. - * Generally it's highly advised to override the {@link DefaultLabeler} whenever possible, + * Generally it's highly advised to override the [DefaultLabeler] whenever possible, * instead of directly implementing the interface. - * <p> + * * As the labeler gets called whenever the state of a message changes, it can also be used * as a listener. - * </p> */ -public interface Labeler { +interface Labeler { /** * Sets the labels of a newly received message. * * @param msg an unlabeled message or broadcast */ - void setLabels(Plaintext msg); + fun setLabels(msg: Plaintext) - void markAsDraft(Plaintext msg); + fun markAsDraft(msg: Plaintext) /** - * It is paramount that this methods marks the {@link Plaintext} object with status - * {@link Plaintext.Status#PUBKEY_REQUESTED} (see {@link DefaultLabeler}) + * It is paramount that this methods marks the [Plaintext] object with status + * [Plaintext.Status.PUBKEY_REQUESTED] (see [DefaultLabeler]) */ - void markAsSending(Plaintext msg); + fun markAsSending(msg: Plaintext) - void markAsSent(Plaintext msg); + fun markAsSent(msg: Plaintext) - void markAsAcknowledged(Plaintext msg); + fun markAsAcknowledged(msg: Plaintext) - void markAsRead(Plaintext msg); + fun markAsRead(msg: Plaintext) - void markAsUnread(Plaintext msg); + fun markAsUnread(msg: Plaintext) - void delete(Plaintext msg); + fun delete(msg: Plaintext) - void archive(Plaintext msg); + fun archive(msg: Plaintext) } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt new file mode 100644 index 0000000..51e0d02 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2017 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.BitmessageAddress +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Status +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.Label +import java.util.* + +interface MessageRepository { + fun getLabels(): List<Label> + + fun getLabels(vararg types: Label.Type): List<Label> + + fun countUnread(label: Label?): Int + + fun getMessage(id: Any): Plaintext + + fun getMessage(iv: InventoryVector): Plaintext? + + fun getMessage(initialHash: ByteArray): Plaintext? + + fun getMessageForAck(ackData: ByteArray): Plaintext? + + /** + * @param label to search for + * * + * @return a distinct list of all conversations that have at least one message with the given label. + */ + fun findConversations(label: Label?): List<UUID> + + fun findMessages(label: Label?): List<Plaintext> + + fun findMessages(status: Status): List<Plaintext> + + fun findMessages(status: Status, recipient: BitmessageAddress): List<Plaintext> + + fun findMessages(sender: BitmessageAddress): List<Plaintext> + + fun findResponses(parent: Plaintext): List<Plaintext> + + fun findMessagesToResend(): List<Plaintext> + + fun save(message: Plaintext) + + fun remove(message: Plaintext) + + /** + * Returns all messages with this conversation ID. The returned messages aren't sorted in any way, + * so you may prefer to use [ch.dissem.bitmessage.utils.ConversationService.getConversation] + * instead. + + * @param conversationId ID of the requested conversation + * * + * @return all messages with the given conversation ID + */ + fun getConversation(conversationId: UUID): Collection<Plaintext> +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.kt new file mode 100644 index 0000000..2f2c8b8 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2017 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.exception.ApplicationException +import ch.dissem.bitmessage.utils.Bytes +import ch.dissem.bitmessage.utils.Bytes.inc +import ch.dissem.bitmessage.utils.ThreadFactoryBuilder.Companion.pool +import org.slf4j.LoggerFactory +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.* +import java.util.concurrent.Callable +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.Future + +/** + * A POW engine using all available CPU cores. + */ +class MultiThreadedPOWEngine : ProofOfWorkEngine { + private val waiterPool = Executors.newSingleThreadExecutor(pool("POW-waiter").daemon().build()) + private val workerPool = Executors.newCachedThreadPool(pool("POW-worker").daemon().build()) + + /** + * 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 fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) { + waiterPool.execute({ + val startTime = System.currentTimeMillis() + + var cores = Runtime.getRuntime().availableProcessors() + if (cores > 255) cores = 255 + LOG.info("Doing POW using $cores cores") + val workers = ArrayList<Worker>(cores) + for (i in 0..cores - 1) { + val w = Worker(cores.toByte(), i, initialHash, target) + workers.add(w) + } + val futures = ArrayList<Future<ByteArray>>(cores) + // 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. + workers.mapTo(futures) { workerPool.submit(it) } + try { + while (!Thread.interrupted()) { + futures.firstOrNull { it.isDone }?.let { + callback.onNonceCalculated(initialHash, it.get()) + LOG.info("Nonce calculated in " + (System.currentTimeMillis() - startTime) / 1000 + " seconds") + futures.forEach { it.cancel(true) } + return@execute + } + } + LOG.error("POW waiter thread interrupted - this should not happen!") + } catch (e: ExecutionException) { + LOG.error(e.message, e) + } catch (e: InterruptedException) { + LOG.error("POW waiter thread interrupted - this should not happen!", e) + } + }) + } + + private inner class Worker internal constructor( + private val numberOfCores: Byte, core: Int, + private val initialHash: ByteArray, + private val target: ByteArray + ) : Callable<ByteArray> { + private val mda: MessageDigest + private val nonce = ByteArray(8) + + init { + this.nonce[7] = core.toByte() + try { + mda = MessageDigest.getInstance("SHA-512") + } catch (e: NoSuchAlgorithmException) { + LOG.error(e.message, e) + throw ApplicationException(e) + } + + } + + override fun call(): ByteArray? { + do { + inc(nonce, numberOfCores) + mda.update(nonce) + mda.update(initialHash) + if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) { + return nonce + } + } while (!Thread.interrupted()) + return null + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine::class.java) + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/NetworkHandler.kt similarity index 58% rename from core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/NetworkHandler.kt index e9c164e..6425fe3 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/NetworkHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,73 +14,73 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; +package ch.dissem.bitmessage.ports -import ch.dissem.bitmessage.entity.CustomMessage; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.utils.Property; +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.Collection; -import java.util.concurrent.Future; +import java.io.IOException +import java.net.InetAddress +import java.util.concurrent.Future /** * Handles incoming messages */ -public interface NetworkHandler { - int NETWORK_MAGIC_NUMBER = 8; - int HEADER_SIZE = 24; - int MAX_PAYLOAD_SIZE = 1600003; - int MAX_MESSAGE_SIZE = HEADER_SIZE + MAX_PAYLOAD_SIZE; +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, long timeoutInSeconds); + fun synchronize(server: InetAddress, port: Int, timeoutInSeconds: Long): Future<*> /** * 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}. - * + * the response, which in turn is expected to be a [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); + fun send(server: InetAddress, port: Int, request: CustomMessage): CustomMessage /** * Start a full network node, accepting incoming connections and relaying objects. */ - void start(); + fun start() /** * Stop the full network node. */ - void stop(); + fun stop() /** * Offer new objects to up to 8 random nodes. */ - void offer(InventoryVector iv); + fun offer(iv: InventoryVector) /** * Request each of those objects from a node that knows of the requested object. - * + * @param inventoryVectors of the objects to be requested */ - void request(Collection<InventoryVector> inventoryVectors); + fun request(inventoryVectors: MutableCollection<InventoryVector>) - Property getNetworkStatus(); + fun getNetworkStatus(): Property - boolean isRunning(); + val isRunning: Boolean interface MessageListener { - void receive(ObjectMessage object) throws IOException; + @Throws(IOException::class) + fun receive(objectMessage: ObjectMessage) } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/NodeRegistry.kt similarity index 69% rename from core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/NodeRegistry.kt index ad5219b..922370b 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/NodeRegistry.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,21 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; +package ch.dissem.bitmessage.ports -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; - -import java.util.List; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress /** * Stores and provides known peers. */ -public interface NodeRegistry { +interface NodeRegistry { /** * Removes all known nodes from registry. This should work around connection issues * when there are many invalid nodes in the registry. */ - void clear(); + fun clear() - List<NetworkAddress> getKnownAddresses(int limit, long... streams); + fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> - void offerAddresses(List<NetworkAddress> addresses); + fun offerAddresses(nodes: List<NetworkAddress>) } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/NodeRegistryHelper.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/NodeRegistryHelper.kt new file mode 100644 index 0000000..921218d --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/NodeRegistryHelper.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.ports + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.utils.UnixTime +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.InetAddress +import java.util.* + +/** + * Helper class to kick start node registries. + */ +object NodeRegistryHelper { + private val LOG = LoggerFactory.getLogger(NodeRegistryHelper::class.java) + + @JvmStatic + fun loadStableNodes(): Map<Long, Set<NetworkAddress>> { + javaClass.classLoader.getResourceAsStream("nodes.txt").use { `in` -> + val scanner = Scanner(`in`) + var stream: Long = 0 + val result = HashMap<Long, Set<NetworkAddress>>() + var streamSet: MutableSet<NetworkAddress>? = null + while (scanner.hasNext()) { + try { + val line = scanner.nextLine().trim { it <= ' ' } + if (line.startsWith("[stream")) { + stream = java.lang.Long.parseLong(line.substring(8, line.lastIndexOf(']'))) + streamSet = HashSet<NetworkAddress>() + result.put(stream, streamSet) + } else if (streamSet != null && !line.isEmpty() && !line.startsWith("#")) { + val portIndex = line.lastIndexOf(':') + val inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex)) + val port = Integer.valueOf(line.substring(portIndex + 1))!! + inetAddresses.mapTo(streamSet) { NetworkAddress( + time = UnixTime.now, + stream = stream, + inetAddress = it, + port = port + ) } + } + } catch (e: IOException) { + LOG.warn(e.message, e) + } + } + if (LOG.isDebugEnabled) { + for ((key, value) in result) { + LOG.debug("Stream " + key + ": loaded " + value.size + " bootstrap nodes.") + } + } + return result + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java b/core/src/main/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkEngine.kt similarity index 77% rename from core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java rename to core/src/main/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkEngine.kt index fc7b4c2..ce76694 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkEngine.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkEngine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,29 @@ * limitations under the License. */ -package ch.dissem.bitmessage.ports; +package ch.dissem.bitmessage.ports /** * Does the proof of work necessary to send an object. */ -public interface ProofOfWorkEngine { +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 + * * * @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make - * sure this is only called once. + * * sure this is only called once. */ - void calculateNonce(byte[] initialHash, byte[] target, Callback callback); + fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: Callback) interface Callback { /** * @param nonce 8 bytes nonce */ - void onNonceCalculated(byte[] initialHash, byte[] nonce); + fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkRepository.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkRepository.kt new file mode 100644 index 0000000..01693eb --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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.Plaintext + +/** + * Objects that proof of work is currently being done for. + + * @author Christian Basler + */ +interface ProofOfWorkRepository { + fun getItem(initialHash: ByteArray): Item + + fun getItems(): List<ByteArray> + + fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) + + fun putObject(item: Item) + + fun removeObject(initialHash: ByteArray) + + data class Item @JvmOverloads constructor( + val objectMessage: ObjectMessage, + val nonceTrialsPerByte: Long, + val extraBytes: Long, + // Needed for ACK POW calculation + val expirationTime: Long? = 0, + val message: Plaintext? = null + ) +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/SimplePOWEngine.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/SimplePOWEngine.kt new file mode 100644 index 0000000..3d7c6d3 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/SimplePOWEngine.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017 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 java.security.MessageDigest + +/** + * You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one. + * + * **Warning:** implementations probably depend on POW being asynchronous, that's + * another reason not to use this one. + */ +class SimplePOWEngine : ProofOfWorkEngine { + override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) { + val mda = MessageDigest.getInstance("SHA-512") + val nonce = ByteArray(8) + do { + Bytes.inc(nonce) + mda.update(nonce) + mda.update(initialHash) + } while (Bytes.lt(target, mda.digest(mda.digest()), 8)) + callback.onNonceCalculated(initialHash, nonce) + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java b/core/src/main/kotlin/ch/dissem/bitmessage/utils/AccessCounter.kt similarity index 52% rename from core/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java rename to core/src/main/kotlin/ch/dissem/bitmessage/utils/AccessCounter.kt index 55c23c3..9da06e8 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/AccessCounter.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/AccessCounter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,48 +14,50 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.utils /** * Intended to count the bytes read or written during (de-)serialization. */ -public class AccessCounter { - private int count; - - /** - * Increases the counter by one, if not null. - */ - public static void inc(AccessCounter counter) { - if (counter != null) counter.inc(); - } - - /** - * Increases the counter by length, if not null. - */ - public static void inc(AccessCounter counter, int length) { - if (counter != null) counter.inc(length); - } +class AccessCounter { + private var count: Int = 0 /** * Increases the counter by one. */ - private void inc() { - count++; + private fun inc() { + count++ } /** * Increases the counter by length. */ - private void inc(int length) { - count += length; + private fun inc(length: Int) { + count += length } - public int length() { - return count; + fun length(): Int { + return count } - @Override - public String toString() { - return String.valueOf(count); + override fun toString(): String { + return count.toString() + } + + companion object { + + /** + * Increases the counter by one, if not null. + */ + @JvmStatic fun inc(counter: AccessCounter?) { + counter?.inc() + } + + /** + * Increases the counter by length, if not null. + */ + @JvmStatic fun inc(counter: AccessCounter?, length: Int) { + counter?.inc(length) + } } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt new file mode 100644 index 0000000..7f98f93 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2017 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.exception.AddressFormatException +import java.util.Arrays.copyOfRange + +/** + * Base58 encoder and decoder. + + * @author Christian Basler: I removed some dependencies to the BitcoinJ code so it can be used here more easily. + */ +object Base58 { + private val INDEXES = IntArray(128) + private val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray() + + init { + for (i in INDEXES.indices) { + INDEXES[i] = -1 + } + for (i in ALPHABET.indices) { + INDEXES[ALPHABET[i].toInt()] = i + } + } + + /** + * Encodes the given bytes in base58. No checksum is appended. + + * @param data to encode + * * + * @return base58 encoded input + */ + @JvmStatic fun encode(data: ByteArray): String { + if (data.isEmpty()) { + return "" + } + val bytes = copyOfRange(data, 0, data.size) + // Count leading zeroes. + var zeroCount = 0 + while (zeroCount < bytes.size && bytes[zeroCount].toInt() == 0) { + ++zeroCount + } + // The actual encoding. + val temp = ByteArray(bytes.size * 2) + var j = temp.size + + var startAt = zeroCount + while (startAt < bytes.size) { + val mod = divmod58(bytes, startAt) + if (bytes[startAt].toInt() == 0) { + ++startAt + } + temp[--j] = ALPHABET[mod.toInt()].toByte() + } + + // Strip extra '1' if there are some after decoding. + while (j < temp.size && temp[j] == ALPHABET[0].toByte()) { + ++j + } + // Add as many leading '1' as there were leading zeros. + while (--zeroCount >= 0) { + temp[--j] = ALPHABET[0].toByte() + } + + val output = copyOfRange(temp, j, temp.size) + return String(output, Charsets.US_ASCII) + } + + @Throws(AddressFormatException::class) + @JvmStatic fun decode(input: String): ByteArray { + if (input.isEmpty()) { + return ByteArray(0) + } + val input58 = ByteArray(input.length) + // Transform the String to a base58 byte sequence + for (i in 0..input.length - 1) { + val c = input[i] + + var digit58 = -1 + if (c.toInt() < 128) { + digit58 = INDEXES[c.toInt()] + } + if (digit58 < 0) { + throw AddressFormatException("Illegal character $c at $i") + } + + input58[i] = digit58.toByte() + } + // Count leading zeroes + var zeroCount = 0 + while (zeroCount < input58.size && input58[zeroCount].toInt() == 0) { + ++zeroCount + } + // The encoding + val temp = ByteArray(input.length) + var j = temp.size + + var startAt = zeroCount + while (startAt < input58.size) { + val mod = divmod256(input58, startAt) + if (input58[startAt].toInt() == 0) { + ++startAt + } + + temp[--j] = mod + } + // Do no add extra leading zeroes, move j to first non null byte. + while (j < temp.size && temp[j].toInt() == 0) { + ++j + } + return copyOfRange(temp, j - zeroCount, temp.size) + } + + // + // number -> number / 58, returns number % 58 + // + private fun divmod58(number: ByteArray, startAt: Int): Byte { + var remainder = 0 + for (i in startAt..number.size - 1) { + val digit256 = number[i].toInt() and 0xFF + val temp = remainder * 256 + digit256 + + number[i] = (temp / 58).toByte() + + remainder = temp % 58 + } + + return remainder.toByte() + } + + // + // number -> number / 256, returns number % 256 + // + private fun divmod256(number58: ByteArray, startAt: Int): Byte { + var remainder = 0 + for (i in startAt..number58.size - 1) { + val digit58 = number58[i].toInt() and 0xFF + val temp = remainder * 58 + digit58 + + number58[i] = (temp / 256).toByte() + + remainder = temp % 256 + } + + return remainder.toByte() + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java b/core/src/main/kotlin/ch/dissem/bitmessage/utils/CallbackWaiter.kt similarity index 52% rename from core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java rename to core/src/main/kotlin/ch/dissem/bitmessage/utils/CallbackWaiter.kt index 775a5f3..14dea31 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/CallbackWaiter.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/CallbackWaiter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,35 +14,32 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +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; +class CallbackWaiter<T> { + private val startTime = System.currentTimeMillis() + @Volatile private var isSet: Boolean = false + private var _value: T? = null + var time: Long = 0 + private set - public void setValue(T value) { - synchronized (this) { - this.time = System.currentTimeMillis() - startTime; - this.value = value; - this.isSet = true; + fun setValue(value: T?) { + synchronized(this) { + this.time = System.currentTimeMillis() - startTime + this._value = value + this.isSet = true } } - public T waitForValue() throws InterruptedException { + fun waitForValue(): T? { while (!isSet) { - Thread.sleep(100); + Thread.sleep(100) } - synchronized (this) { - return value; + synchronized(this) { + return _value } } - - public long getTime() { - return time; - } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Collections.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Collections.kt new file mode 100644 index 0000000..47626d2 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Collections.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2017 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.* + +object Collections { + private val RANDOM = Random() + + /** + * @param count the number of elements to return (if possible) + * * + * @param collection the collection to take samples from + * * + * @return a random subset of the given collection, or a copy of the collection if it's not larger than count. The + * * result is by no means securely random, but should be random enough so not the same objects get selected over + * * and over again. + */ + @JvmStatic fun <T> selectRandom(count: Int, collection: Collection<T>): List<T> { + val result = ArrayList<T>(count) + if (collection.size <= count) { + result.addAll(collection) + } else { + var collectionRest = collection.size.toDouble() + var resultRest = count.toDouble() + var skipMax = Math.ceil(collectionRest / resultRest).toInt() + var skip = RANDOM.nextInt(skipMax) + for (item in collection) { + collectionRest-- + if (skip > 0) { + skip-- + } else { + result.add(item) + resultRest-- + if (resultRest == 0.0) { + break + } + skipMax = Math.ceil(collectionRest / resultRest).toInt() + skip = RANDOM.nextInt(skipMax) + } + } + } + return result + } + + @JvmStatic fun <T> selectRandom(collection: Collection<T>): T { + var index = RANDOM.nextInt(collection.size) + for (item in collection) { + if (index == 0) { + return item + } + index-- + } + throw IllegalArgumentException("Empty collection? Size: " + collection.size) + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/ConversationService.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/ConversationService.kt new file mode 100644 index 0000000..f06a6ed --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/ConversationService.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2017 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.Plaintext +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.ports.MessageRepository +import org.slf4j.LoggerFactory +import java.util.* +import java.util.Collections +import java.util.regex.Pattern +import java.util.regex.Pattern.CASE_INSENSITIVE + +/** + * Service that helps with conversations. + */ +class ConversationService(private val messageRepository: MessageRepository) { + + private val SUBJECT_PREFIX = Pattern.compile("^(re|fwd?):", CASE_INSENSITIVE) + + /** + * Retrieve the whole conversation from one single message. If the message isn't part + * of a conversation, a singleton list containing the given message is returned. Otherwise + * it's the same as [.getConversation] + + * @param message + * * + * @return a list of messages that belong to the same conversation. + */ + fun getConversation(message: Plaintext): List<Plaintext> { + return getConversation(message.conversationId) + } + + private fun sorted(collection: Collection<Plaintext>): LinkedList<Plaintext> { + val result = LinkedList(collection) + Collections.sort(result, Comparator<Plaintext> { o1, o2 -> + return@Comparator when { + o1.received === o2.received -> 0 + o1.received == null -> -1 + o2.received == null -> 1 + else -> -o1.received.compareTo(o2.received) + } + }) + return result + } + + fun getConversation(conversationId: UUID): List<Plaintext> { + val messages = sorted(messageRepository.getConversation(conversationId)) + val map = HashMap<InventoryVector, Plaintext>(messages.size) + for (message in messages) { + message.inventoryVector?.let { + map.put(it, message) + } + } + + val result = LinkedList<Plaintext>() + while (!messages.isEmpty()) { + val last = messages.poll() + val pos = lastParentPosition(last, result) + result.add(pos, last) + addAncestors(last, result, messages, map) + } + return result + } + + fun getSubject(conversation: List<Plaintext>): String? { + if (conversation.isEmpty()) { + return null + } + // TODO: this has room for improvement + val subject = conversation[0].subject + val matcher = SUBJECT_PREFIX.matcher(subject!!) + + return if (matcher.find()) { + subject.substring(matcher.end()) + } else { + subject + }.trim { it <= ' ' } + } + + private fun lastParentPosition(child: Plaintext, messages: LinkedList<Plaintext>): Int { + val plaintextIterator = messages.descendingIterator() + var i = 0 + while (plaintextIterator.hasNext()) { + val next = plaintextIterator.next() + if (isParent(next, child)) { + break + } + i++ + } + return messages.size - i + } + + private fun isParent(item: Plaintext, child: Plaintext): Boolean { + return child.parents.firstOrNull { it == item.inventoryVector } != null + } + + private fun addAncestors(message: Plaintext, result: LinkedList<Plaintext>, messages: LinkedList<Plaintext>, map: MutableMap<InventoryVector, Plaintext>) { + for (parentKey in message.parents) { + map.remove(parentKey)?.let { + messages.remove(it) + result.addFirst(it) + addAncestors(it, result, messages, map) + } + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/DebugUtils.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/DebugUtils.kt new file mode 100644 index 0000000..acbbabf --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/DebugUtils.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017 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 org.slf4j.LoggerFactory +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +object DebugUtils { + private val LOG = LoggerFactory.getLogger(DebugUtils::class.java) + + @JvmStatic fun saveToFile(objectMessage: ObjectMessage) { + try { + val f = File(System.getProperty("user.home") + "/jabit.error/" + objectMessage.inventoryVector + ".inv") + f.createNewFile() + objectMessage.write(FileOutputStream(f)) + } catch (e: IOException) { + LOG.debug(e.message, e) + } + + } + + @JvmStatic fun <K> inc(map: MutableMap<K, Int>, key: K) { + val value = map[key] + if (value == null) { + map.put(key, 1) + } else { + map.put(key, value + 1) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Decode.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Decode.kt new file mode 100644 index 0000000..3eb192e --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Decode.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2017 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.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * This class handles decoding simple types from byte stream, according to + * https://bitmessage.org/wiki/Protocol_specification#Common_structures + */ +object Decode { + @JvmStatic fun shortVarBytes(`in`: InputStream, counter: AccessCounter): ByteArray { + val length = uint16(`in`, counter) + return bytes(`in`, length, counter) + } + + @JvmStatic @JvmOverloads fun varBytes(`in`: InputStream, counter: AccessCounter? = null): ByteArray { + val length = varInt(`in`, counter).toInt() + return bytes(`in`, length, counter) + } + + @JvmStatic @JvmOverloads fun bytes(`in`: InputStream, count: Int, counter: AccessCounter? = null): ByteArray { + val result = ByteArray(count) + var off = 0 + while (off < count) { + val read = `in`.read(result, off, count - off) + if (read < 0) { + throw IOException("Unexpected end of stream, wanted to read $count bytes but only got $off") + } + off += read + } + AccessCounter.inc(counter, count) + return result + } + + @JvmStatic fun varIntList(`in`: InputStream): LongArray { + val length = varInt(`in`).toInt() + val result = LongArray(length) + + for (i in 0..length - 1) { + result[i] = varInt(`in`) + } + return result + } + + @JvmStatic @JvmOverloads fun varInt(`in`: InputStream, counter: AccessCounter? = null): Long { + val first = `in`.read() + AccessCounter.inc(counter) + when (first) { + 0xfd -> return uint16(`in`, counter).toLong() + 0xfe -> return uint32(`in`, counter) + 0xff -> return int64(`in`, counter) + else -> return first.toLong() + } + } + + @JvmStatic fun uint8(`in`: InputStream): Int { + return `in`.read() + } + + @JvmStatic @JvmOverloads fun uint16(`in`: InputStream, counter: AccessCounter? = null): Int { + AccessCounter.inc(counter, 2) + return `in`.read() shl 8 or `in`.read() + } + + @JvmStatic @JvmOverloads fun uint32(`in`: InputStream, counter: AccessCounter? = null): Long { + AccessCounter.inc(counter, 4) + return (`in`.read() shl 24 or (`in`.read() shl 16) or (`in`.read() shl 8) or `in`.read()).toLong() + } + + @JvmStatic fun uint32(`in`: ByteBuffer): Long { + return (u(`in`.get()) shl 24 or (u(`in`.get()) shl 16) or (u(`in`.get()) shl 8) or u(`in`.get())).toLong() + } + + @JvmStatic @JvmOverloads fun int32(`in`: InputStream, counter: AccessCounter? = null): Int { + AccessCounter.inc(counter, 4) + return ByteBuffer.wrap(bytes(`in`, 4)).int + } + + @JvmStatic @JvmOverloads fun int64(`in`: InputStream, counter: AccessCounter? = null): Long { + AccessCounter.inc(counter, 8) + return ByteBuffer.wrap(bytes(`in`, 8)).long + } + + @JvmStatic @JvmOverloads fun varString(`in`: InputStream, counter: AccessCounter? = null): String { + val length = varInt(`in`, counter).toInt() + // 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 String(bytes(`in`, length, counter)) + } + + /** + * Returns the given byte as if it were unsigned. + */ + @JvmStatic private fun u(b: Byte): Int { + return b.toInt() and 0xFF + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Encode.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Encode.kt new file mode 100644 index 0000000..1b96935 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Encode.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2017 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.Streamable +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * This class handles encoding simple types from byte stream, according to + * https://bitmessage.org/wiki/Protocol_specification#Common_structures + */ +object Encode { + @JvmStatic fun varIntList(values: LongArray, stream: OutputStream) { + varInt(values.size, stream) + for (value in values) { + varInt(value, stream) + } + } + + @JvmStatic fun varIntList(values: LongArray, buffer: ByteBuffer) { + varInt(values.size, buffer) + for (value in values) { + varInt(value, buffer) + } + } + + @JvmStatic fun varInt(value: Int, buffer: ByteBuffer) = varInt(value.toLong(), buffer) + @JvmStatic fun varInt(value: Long, buffer: ByteBuffer) { + if (value < 0) { + // This is due to the fact that Java doesn't really support unsigned values. + // Please be aware that this might be an error due to a smaller negative value being cast to long. + // Normally, negative values shouldn't occur within the protocol, and longs large enough for being + // recognized as negatives aren't realistic. + buffer.put(0xff.toByte()) + buffer.putLong(value) + } else if (value < 0xfd) { + buffer.put(value.toByte()) + } else if (value <= 0xffffL) { + buffer.put(0xfd.toByte()) + buffer.putShort(value.toShort()) + } else if (value <= 0xffffffffL) { + buffer.put(0xfe.toByte()) + buffer.putInt(value.toInt()) + } else { + buffer.put(0xff.toByte()) + buffer.putLong(value) + } + } + + @JvmStatic fun varInt(value: Int) = varInt(value.toLong()) + @JvmStatic fun varInt(value: Long): ByteArray { + val buffer = ByteBuffer.allocate(9) + varInt(value, buffer) + buffer.flip() + return Bytes.truncate(buffer.array(), buffer.limit()) + } + + @JvmStatic @JvmOverloads fun varInt(value: Int, stream: OutputStream, counter: AccessCounter? = null) = varInt(value.toLong(), stream, counter) + @JvmStatic @JvmOverloads fun varInt(value: Long, stream: OutputStream, counter: AccessCounter? = null) { + val buffer = ByteBuffer.allocate(9) + varInt(value, buffer) + buffer.flip() + stream.write(buffer.array(), 0, buffer.limit()) + AccessCounter.inc(counter, buffer.limit()) + } + + @JvmStatic @JvmOverloads fun int8(value: Long, stream: OutputStream, counter: AccessCounter? = null) = int8(value.toInt(), stream, counter) + @JvmStatic @JvmOverloads fun int8(value: Int, stream: OutputStream, counter: AccessCounter? = null) { + stream.write(value) + AccessCounter.inc(counter) + } + + @JvmStatic @JvmOverloads fun int16(value: Long, stream: OutputStream, counter: AccessCounter? = null) = int16(value.toShort(), stream, counter) + @JvmStatic @JvmOverloads fun int16(value: Int, stream: OutputStream, counter: AccessCounter? = null) = int16(value.toShort(), stream, counter) + @JvmStatic @JvmOverloads fun int16(value: Short, stream: OutputStream, counter: AccessCounter? = null) { + stream.write(ByteBuffer.allocate(2).putShort(value).array()) + AccessCounter.inc(counter, 2) + } + + @JvmStatic fun int16(value: Long, buffer: ByteBuffer) = int16(value.toShort(), buffer) + @JvmStatic fun int16(value: Int, buffer: ByteBuffer) = int16(value.toShort(), buffer) + @JvmStatic fun int16(value: Short, buffer: ByteBuffer) { + buffer.putShort(value) + } + + @JvmStatic @JvmOverloads fun int32(value: Long, stream: OutputStream, counter: AccessCounter? = null) = int32(value.toInt(), stream, counter) + @JvmStatic @JvmOverloads fun int32(value: Int, stream: OutputStream, counter: AccessCounter? = null) { + stream.write(ByteBuffer.allocate(4).putInt(value).array()) + AccessCounter.inc(counter, 4) + } + + @JvmStatic fun int32(value: Long, buffer: ByteBuffer) = int32(value.toInt(), buffer) + @JvmStatic fun int32(value: Int, buffer: ByteBuffer) { + buffer.putInt(value) + } + + @JvmStatic @JvmOverloads fun int64(value: Long, stream: OutputStream, counter: AccessCounter? = null) { + stream.write(ByteBuffer.allocate(8).putLong(value).array()) + AccessCounter.inc(counter, 8) + } + + @JvmStatic fun int64(value: Long, buffer: ByteBuffer) { + buffer.putLong(value) + } + + @JvmStatic fun varString(value: String, out: OutputStream) { + val bytes = value.toByteArray(charset("utf-8")) + // 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.size.toLong(), out) + out.write(bytes) + } + + @JvmStatic fun varString(value: String, buffer: ByteBuffer) { + val bytes = value.toByteArray() + // 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() + buffer.put(varInt(bytes.size.toLong())) + buffer.put(bytes) + } + + @JvmStatic fun varBytes(data: ByteArray, out: OutputStream) { + varInt(data.size.toLong(), out) + out.write(data) + } + + @JvmStatic fun varBytes(data: ByteArray, buffer: ByteBuffer) { + varInt(data.size.toLong(), buffer) + buffer.put(data) + } + + /** + * Serializes a [Streamable] object and returns the byte array. + * @param streamable the object to be serialized + * @return an array of bytes representing the given streamable object. + */ + @JvmStatic fun bytes(streamable: Streamable): ByteArray { + val stream = ByteArrayOutputStream() + streamable.write(stream) + return stream.toByteArray() + } + + /** + * @param streamable the object to be serialized + * @param padding the result will be padded such that its length is a multiple of *padding* + * @return the bytes of the given [Streamable] object, 0-padded such that the final length is x*padding. + */ + @JvmStatic fun bytes(streamable: Streamable, padding: Int): ByteArray { + val stream = ByteArrayOutputStream() + streamable.write(stream) + val offset = padding - stream.size() % padding + val length = stream.size() + offset + val result = ByteArray(length) + stream.write(result, offset, stream.size()) + return result + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Numbers.kt similarity index 73% rename from core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java rename to core/src/main/kotlin/ch/dissem/bitmessage/utils/Numbers.kt index c9e3efd..445cbd1 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Numbers.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,10 @@ * limitations under the License. */ -package ch.dissem.bitmessage.exception; +@file:JvmName("Numbers") -public class DecryptionFailedException extends Exception { - private static final long serialVersionUID = 3241116253113872731L; +package ch.dissem.bitmessage.utils + +fun max(a: Long, b: Long): Long { + return if (a > b) a else b } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Points.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Points.kt new file mode 100644 index 0000000..7f07188 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Points.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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 + +/** + * Helper object to get a point from a public key on a elliptic curve. + */ +object Points { + /** + * returns X component of the point represented by public key P + */ + @JvmStatic fun getX(P: ByteArray): ByteArray { + return P.sliceArray(1..(P.size - 1) / 2) + } + + /** + * returns Y component of the point represented by public key P + */ + @JvmStatic fun getY(P: ByteArray): ByteArray { + return P.sliceArray((P.size - 1) / 2 + 1..P.size - 1) + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Property.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Property.kt new file mode 100644 index 0000000..91fbb24 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Property.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2017 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 + +/** + * 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. + * + * + * If you need a real JSON representation, please add a method `toJson()`. + * + */ +class Property private constructor( + val name: String, + val value: Any? = null, + val properties: Array<Property> +) { + + constructor(name: String, value: Any) : this(name = name, value = value, properties = emptyArray()) + constructor(name: String, vararg properties: Property) : this(name, null, arrayOf(*properties)) + constructor(name: String, properties: List<Property>) : this(name, null, properties.toTypedArray()) + + /** + * Returns the property if available or `null` otherwise. + * Subproperties can be requested by submitting the sequence of properties. + */ + fun getProperty(vararg name: String): Property? { + properties + .filter { name[0] == it.name } + .forEach { + if (name.size == 1) + return it + else + return it.getProperty(*name.sliceArray(1..name.size - 1)) + } + return null + } + + override fun toString(): String { + return toString("") + } + + private fun toString(indentation: String): String { + val result = StringBuilder() + result.append(indentation).append(name).append(": ") + if (value != null || properties.isEmpty()) { + result.append(value) + } + if (properties.isNotEmpty()) { + result.append("{\n") + for (property in properties) { + result.append(property.toString(indentation + " ")).append('\n') + } + result.append(indentation).append("}") + } + return result.toString() + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Singleton.kt similarity index 58% rename from core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java rename to core/src/main/kotlin/ch/dissem/bitmessage/utils/Singleton.kt index 2eeaa97..bd7e4d7 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Singleton.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,23 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.utils -import ch.dissem.bitmessage.ports.Cryptography; +import ch.dissem.bitmessage.ports.Cryptography +import kotlin.properties.Delegates /** * @author Christian Basler */ -public class Singleton { - private static Cryptography cryptography; +object Singleton { + private var cryptography by Delegates.notNull<Cryptography>() - public static void initialize(Cryptography cryptography) { - synchronized (Singleton.class) { - Singleton.cryptography = cryptography; - } + @Synchronized + @JvmStatic fun initialize(cryptography: Cryptography) { + Singleton.cryptography = cryptography } - public static Cryptography cryptography() { - return cryptography; + @JvmStatic fun cryptography(): Cryptography { + return cryptography } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/SqlStrings.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/SqlStrings.kt new file mode 100644 index 0000000..d0efdf0 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/SqlStrings.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils + +import ch.dissem.bitmessage.entity.payload.ObjectType + +object SqlStrings { + @JvmStatic fun join(vararg objects: Long): String { + return objects.joinToString() + } + + @JvmStatic fun join(vararg objects: ByteArray): String { + return objects.map { Strings.hex(it) }.joinToString() + } + + @JvmStatic fun join(vararg types: ObjectType): String { + return types.map { it.number }.joinToString() + } + + @JvmStatic fun join(vararg types: Enum<*>): String { + return types.map { '\'' + it.name + '\'' }.joinToString() + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Strings.kt similarity index 50% rename from core/src/main/java/ch/dissem/bitmessage/utils/Strings.java rename to core/src/main/kotlin/ch/dissem/bitmessage/utils/Strings.kt index 96383f7..98fd16e 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Strings.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,31 +14,27 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.utils /** * Some utilities to handle strings. * TODO: Probably this should be split in a GUI related and an SQL related utility class. */ -public class Strings { - public static StringBuilder join(Object... objects) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < objects.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(objects[i]); - } - return streamList; +object Strings { + @JvmStatic fun join(vararg objects: Any): String { + return objects.joinToString() } - public static StringBuilder hex(byte[] bytes) { - StringBuilder hex = new StringBuilder(bytes.length + 2); - for (byte b : bytes) { - hex.append(String.format("%02x", b)); - } - return hex; + @JvmStatic fun hex(bytes: ByteArray): String { + return bytes.map { String.format("%02x", it) }.joinToString(separator = "") } - public static String str(Object o) { - return o == null ? null : o.toString(); + @JvmStatic fun str(o: Any?): String? { + return o?.toString() + } + + @JvmName("strNonNull") + @JvmStatic fun str(o: Any): String { + return o.toString() } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/TTL.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/TTL.kt new file mode 100644 index 0000000..6fe883a --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/TTL.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2017 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 + +/** + * Stores times to live in seconds for different object types. Usually this shouldn't be messed with, but for tests + * it might be a good idea to reduce it to a minimum, and on mobile clients you might want to optimize it as well. + + * @author Christian Basler + */ +object TTL { + @JvmStatic var msg = 2 * UnixTime.DAY + @JvmName("msg") get + @JvmName("msg") set(msg) { + field = validate(msg) + } + + @JvmStatic var getpubkey = 2 * UnixTime.DAY + @JvmName("getpubkey") get + @JvmName("getpubkey") set(getpubkey) { + field = validate(getpubkey) + } + + @JvmStatic var pubkey = 28 * UnixTime.DAY + @JvmName("pubkey") get + @JvmName("pubkey") set(pubkey) { + field = validate(pubkey) + } + + private fun validate(ttl: Long): Long { + if (ttl < 0 || ttl > 28 * UnixTime.DAY) + throw IllegalArgumentException("TTL must be between 0 seconds and 28 days") + return ttl + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.kt new file mode 100644 index 0000000..98bfa55 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2017 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.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + +class ThreadFactoryBuilder private constructor(pool: String) { + private val namePrefix: String = pool + "-thread-" + private var prio = Thread.NORM_PRIORITY + private var daemon = false + + fun lowPrio(): ThreadFactoryBuilder { + prio = Thread.MIN_PRIORITY + return this + } + + fun daemon(): ThreadFactoryBuilder { + daemon = true + return this + } + + fun build(): ThreadFactory { + val s = System.getSecurityManager() + val group = if (s != null) + s.threadGroup + else + Thread.currentThread().threadGroup + + return object : ThreadFactory { + private val threadNumber = AtomicInteger(1) + + override fun newThread(r: Runnable): Thread { + val t = Thread(group, r, + namePrefix + threadNumber.getAndIncrement(), + 0) + t.priority = prio + t.isDaemon = daemon + return t + } + } + } + + companion object { + @JvmStatic fun pool(name: String): ThreadFactoryBuilder { + return ThreadFactoryBuilder(name) + } + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt new file mode 100644 index 0000000..d21bc2e --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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 + +/** + * A simple utility class that simplifies using the second based time used in Bitmessage. + */ +object UnixTime { + /** + * Length of a minute in seconds, intended for use with [.now]. + */ + @JvmField val MINUTE = 60L + /** + * Length of an hour in seconds, intended for use with [.now]. + */ + @JvmField val HOUR = 60L * MINUTE + /** + * Length of a day in seconds, intended for use with [.now]. + */ + @JvmField val DAY = 24L * HOUR + + /** + * @return the time in second based Unix time ([System.currentTimeMillis]/1000) + */ + @JvmStatic val now: Long + @JvmName("now") get() { + return System.currentTimeMillis() / 1000L + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java deleted file mode 100644 index 0702866..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage; - -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.Plaintext.Type; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.testutils.TestInventory; -import ch.dissem.bitmessage.utils.MessageMatchers; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.TTL; -import ch.dissem.bitmessage.utils.TestUtils; -import org.junit.Before; -import org.junit.Test; - -import java.util.*; - -import static ch.dissem.bitmessage.entity.payload.ObjectType.*; -import static ch.dissem.bitmessage.utils.MessageMatchers.object; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -/** - * @author Christian Basler - */ -public class BitmessageContextTest { - private BitmessageContext ctx; - private BitmessageContext.Listener listener; - private TestInventory testInventory; - - @Before - public void setUp() throws Exception { - Singleton.initialize(null); - listener = mock(BitmessageContext.Listener.class); - Singleton.initialize(new BouncyCryptography()); - testInventory = new TestInventory(); - ctx = new BitmessageContext.Builder() - .addressRepo(mock(AddressRepository.class)) - .cryptography(cryptography()) - .inventory(spy(testInventory)) - .listener(listener) - .messageRepo(mock(MessageRepository.class)) - .networkHandler(mock(NetworkHandler.class)) - .nodeRegistry(mock(NodeRegistry.class)) - .labeler(spy(new DefaultLabeler())) - .powRepo(spy(new ProofOfWorkRepository() { - Map<InventoryVector, Item> items = new HashMap<>(); - - @Override - public Item getItem(byte[] initialHash) { - return items.get(InventoryVector.fromHash(initialHash)); - } - - @Override - public List<byte[]> getItems() { - List<byte[]> result = new LinkedList<>(); - for (InventoryVector iv : items.keySet()) { - result.add(iv.getHash()); - } - return result; - } - - @Override - public void putObject(Item item) { - items.put(InventoryVector.fromHash(cryptography().getInitialHash(item.object)), item); - } - - @Override - public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - items.put(InventoryVector.fromHash(cryptography().getInitialHash(object)), new Item(object, nonceTrialsPerByte, extraBytes)); - } - - @Override - public void removeObject(byte[] initialHash) { - items.remove(initialHash); - } - })) - .proofOfWorkEngine(spy(new ProofOfWorkEngine() { - @Override - public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { - callback.onNonceCalculated(initialHash, new byte[8]); - } - })) - .build(); - TTL.msg(2 * MINUTE); - } - - @Test - public void ensureContactIsSavedAndPubkeyRequested() { - BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(contact); - ctx.addContact(contact); - - verify(ctx.addresses(), timeout(1000).atLeastOnce()).save(contact); - verify(ctx.internals().getProofOfWorkEngine(), timeout(1000)) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); - } - - @Test - public void ensurePubkeyIsNotRequestedIfItExists() throws Exception { - ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); - Pubkey pubkey = (Pubkey) object.getPayload(); - BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - contact.setPubkey(pubkey); - - ctx.addContact(contact); - - verify(ctx.addresses(), times(1)).save(contact); - verify(ctx.internals().getProofOfWorkEngine(), never()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); - } - - @Test - public void ensureV2PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { - testInventory.init( - "V1Msg.payload", - "V2GetPubkey.payload", - "V2Pubkey.payload", - "V3GetPubkey.payload", - "V3Pubkey.payload", - "V4Broadcast.payload", - "V4GetPubkey.payload", - "V4Pubkey.payload", - "V5Broadcast.payload" - ); - BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - - when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(contact); - - ctx.addContact(contact); - - verify(ctx.addresses(), atLeastOnce()).save(contact); - verify(ctx.internals().getProofOfWorkEngine(), never()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); - } - - @Test - public void ensureV4PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { - testInventory.init( - "V1Msg.payload", - "V2GetPubkey.payload", - "V2Pubkey.payload", - "V3GetPubkey.payload", - "V3Pubkey.payload", - "V4Broadcast.payload", - "V4GetPubkey.payload", - "V4Pubkey.payload", - "V5Broadcast.payload" - ); - BitmessageAddress contact = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - final BitmessageAddress stored = new BitmessageAddress(contact.getAddress()); - stored.setAlias("Test"); - when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(stored); - - ctx.addContact(contact); - - verify(ctx.addresses(), atLeastOnce()).save(any(BitmessageAddress.class)); - verify(ctx.internals().getProofOfWorkEngine(), never()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); - } - - @Test - public void ensureSubscriptionIsAddedAndExistingBroadcastsRetrieved() throws Exception { - BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - - testInventory.init( - "V4Broadcast.payload", - "V5Broadcast.payload" - ); - - when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address)); - ctx.addSubscribtion(address); - - verify(ctx.addresses(), atLeastOnce()).save(address); - assertThat(address.isSubscribed(), is(true)); - verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)); - verify(listener).receive(any(Plaintext.class)); - } - - @Test - public void ensureIdentityIsCreated() { - assertThat(ctx.createIdentity(false), notNullValue()); - } - - @Test - public void ensureMessageIsSent() throws Exception { - ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(), - "Subject", "Message"); - assertEquals(2, ctx.internals().getProofOfWorkRepository().getItems().size()); - verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) - .putObject(object(MSG), eq(1000L), eq(1000L)); - verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG)); - } - - @Test - public void ensurePubkeyIsRequestedIfItIsMissing() throws Exception { - ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), - new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), - "Subject", "Message"); - verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) - .putObject(object(GET_PUBKEY), eq(1000L), eq(1000L)); - verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG)); - } - - @Test(expected = IllegalArgumentException.class) - public void ensureSenderMustBeIdentity() { - ctx.send(new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), - new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), - "Subject", "Message"); - } - - @Test - public void ensureBroadcastIsSent() throws Exception { - ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), - "Subject", "Message"); - verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) - .putObject(object(BROADCAST), eq(1000L), eq(1000L)); - verify(ctx.internals().getProofOfWorkEngine()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); - verify(ctx.messages(), timeout(10000).atLeastOnce()) - .save(MessageMatchers.plaintext(Type.BROADCAST)); - } - - @Test(expected = IllegalArgumentException.class) - public void ensureSenderWithoutPrivateKeyThrowsException() { - Plaintext msg = new Plaintext.Builder(Type.BROADCAST) - .from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .message("Subject", "Message") - .build(); - ctx.send(msg); - } - - @Test - public void ensureChanIsJoined() { - String chanAddress = "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r"; - BitmessageAddress chan = ctx.joinChan("general", chanAddress); - assertNotNull(chan); - assertEquals(chan.getAddress(), chanAddress); - assertTrue(chan.isChan()); - } - - @Test - public void ensureDeterministicAddressesAreCreated() { - final int expected_size = 8; - List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, false); - assertEquals(expected_size, addresses.size()); - Set<String> expected = new HashSet<>(expected_size); - expected.add("BM-2cWFkyuXXFw6d393RGnin2RpSXj8wxtt6F"); - expected.add("BM-2cX8TF9vuQZEWvT7UrEeq1HN9dgiSUPLEN"); - expected.add("BM-2cUzX8f9CKUU7L8NeB8GExZvf54PrcXq1S"); - expected.add("BM-2cU7MAoQd7KE8SPF7AKFPpoEZKjk86KRqE"); - expected.add("BM-2cVm8ByVBacc2DVhdTNs6rmy5ZQK6DUsrt"); - expected.add("BM-2cW2af1vB6kWon2WkygDHqGwfcpfAFm2Jk"); - expected.add("BM-2cWdWD7UtUN4gWChgNX9pvyvNPjUZvU8BT"); - expected.add("BM-2cXkYgYcUrv4fGxSHzyEScW955Cc8sDteo"); - for (BitmessageAddress a : addresses) { - assertTrue(expected.contains(a.getAddress())); - expected.remove(a.getAddress()); - } - } - - @Test - public void ensureShortDeterministicAddressesAreCreated() { - final int expected_size = 1; - List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, true); - assertEquals(expected_size, addresses.size()); - Set<String> expected = new HashSet<>(expected_size); - expected.add("BM-NBGyBAEp6VnBkFWKpzUSgxuTqVdWPi78"); - for (BitmessageAddress a : addresses) { - assertTrue(expected.contains(a.getAddress())); - expected.remove(a.getAddress()); - } - } - - @Test - public void ensureChanIsCreated() { - BitmessageAddress chan = ctx.createChan("test"); - assertNotNull(chan); - assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION); - assertTrue(chan.isChan()); - } - - @Test - public void ensureUnacknowledgedMessageIsResent() throws Exception { - Plaintext plaintext = new Plaintext.Builder(Type.MSG) - .ttl(1) - .message("subject", "message") - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .build(); - assertTrue(plaintext.getTo().has(Pubkey.Feature.DOES_ACK)); - when(ctx.messages().findMessagesToResend()).thenReturn(Collections.singletonList(plaintext)); - when(ctx.messages().getMessage(any(byte[].class))).thenReturn(plaintext); - ctx.resendUnacknowledgedMessages(); - verify(ctx.labeler(), timeout(1000).times(1)).markAsSent(eq(plaintext)); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java deleted file mode 100644 index 21f9506..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/DecryptionTest.java +++ /dev/null @@ -1,55 +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.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; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class DecryptionTest extends TestBase { - @Test - public void ensureV4BroadcastIsDecryptedCorrectly() throws IOException, DecryptionFailedException { - BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - TestUtils.loadPubkey(address); - ObjectMessage objectMessage = TestUtils.loadObjectMessage(5, "V4Broadcast.payload"); - V4Broadcast broadcast = (V4Broadcast) objectMessage.getPayload(); - broadcast.decrypt(address); - assertEquals("Test-Broadcast", broadcast.getPlaintext().getSubject()); - assertTrue(objectMessage.isSignatureValid(address.getPubkey())); - } - - @Test - public void ensureV5BroadcastIsDecryptedCorrectly() throws IOException, DecryptionFailedException { - BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - TestUtils.loadPubkey(address); - ObjectMessage objectMessage = TestUtils.loadObjectMessage(5, "V5Broadcast.payload"); - V5Broadcast broadcast = (V5Broadcast) objectMessage.getPayload(); - broadcast.decrypt(address); - assertEquals("Test-Broadcast", broadcast.getPlaintext().getSubject()); - assertTrue(objectMessage.isSignatureValid(address.getPubkey())); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java b/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java deleted file mode 100644 index 32a20e4..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage; - -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.Broadcast; -import ch.dissem.bitmessage.entity.payload.GetPubkey; -import ch.dissem.bitmessage.entity.payload.Msg; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.TestBase; -import ch.dissem.bitmessage.utils.TestUtils; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Collections; - -import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED; -import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.MessageMatchers.plaintext; -import static org.mockito.Mockito.*; - -/** - * @author Christian Basler - */ -public class DefaultMessageListenerTest extends TestBase { - @Mock - private AddressRepository addressRepo; - @Mock - private MessageRepository messageRepo; - @Mock - private Inventory inventory; - @Mock - private NetworkHandler networkHandler; - - private InternalContext ctx; - private DefaultMessageListener listener; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - ctx = mock(InternalContext.class); - Singleton.initialize(new BouncyCryptography()); - when(ctx.getAddressRepository()).thenReturn(addressRepo); - when(ctx.getMessageRepository()).thenReturn(messageRepo); - when(ctx.getInventory()).thenReturn(inventory); - when(ctx.getNetworkHandler()).thenReturn(networkHandler); - when(ctx.getLabeler()).thenReturn(mock(Labeler.class)); - - listener = new DefaultMessageListener(mock(Labeler.class), mock(BitmessageContext.Listener.class)); - when(ctx.getNetworkListener()).thenReturn(listener); - listener.setContext(ctx); - } - - @Test - public void ensurePubkeyIsSentOnRequest() throws Exception { - BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - when(addressRepo.findIdentity(any(byte[].class))) - .thenReturn(identity); - listener.receive(new ObjectMessage.Builder() - .stream(2) - .payload(new GetPubkey(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))) - .build()); - verify(ctx).sendPubkey(eq(identity), eq(2L)); - } - - @Test - public void ensureIncomingPubkeyIsAddedToContact() throws Exception { - BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); - when(addressRepo.findContact(any(byte[].class))) - .thenReturn(contact); - when(messageRepo.findMessages(eq(PUBKEY_REQUESTED), eq(contact))) - .thenReturn(Collections.singletonList( - new Plaintext.Builder(MSG).from(identity).to(contact).message("S", "T").build() - )); - - ObjectMessage objectMessage = new ObjectMessage.Builder() - .stream(2) - .payload(identity.getPubkey()) - .build(); - objectMessage.sign(identity.getPrivateKey()); - objectMessage.encrypt(Singleton.cryptography().createPublicKey(identity.getPublicDecryptionKey())); - listener.receive(objectMessage); - - verify(addressRepo).save(any(BitmessageAddress.class)); - } - - @Test - public void ensureIncomingMessageIsSaved() throws Exception { - BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); - contact.setPubkey(identity.getPubkey()); - - when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity)); - - ObjectMessage objectMessage = new ObjectMessage.Builder() - .stream(2) - .payload(new Msg(new Plaintext.Builder(MSG) - .from(identity) - .to(contact) - .message("S", "T") - .build())) - .nonce(new byte[8]) - .build(); - objectMessage.sign(identity.getPrivateKey()); - objectMessage.encrypt(identity.getPubkey()); - - listener.receive(objectMessage); - - verify(messageRepo, atLeastOnce()).save(plaintext(MSG)); - } - - @Test - public void ensureIncomingBroadcastIsSaved() throws Exception { - BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - - when(addressRepo.getSubscriptions(anyLong())).thenReturn(Collections.singletonList(identity)); - - Broadcast broadcast = Factory.getBroadcast(new Plaintext.Builder(BROADCAST) - .from(identity) - .message("S", "T") - .build()); - ObjectMessage objectMessage = new ObjectMessage.Builder() - .stream(2) - .payload(broadcast) - .nonce(new byte[8]) - .build(); - objectMessage.sign(identity.getPrivateKey()); - broadcast.encrypt(); - - listener.receive(objectMessage); - - verify(messageRepo, atLeastOnce()).save(plaintext(BROADCAST)); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java deleted file mode 100644 index 0a6ee25..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java +++ /dev/null @@ -1,64 +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.CryptoBox; -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.TestBase; -import ch.dissem.bitmessage.utils.TestUtils; -import org.junit.Test; - -import java.io.IOException; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class EncryptionTest extends TestBase { - @Test - public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException { - GenericPayload before = new GenericPayload(0, 1, cryptography().randomBytes(100)); - - PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); - CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey()); - - GenericPayload after = GenericPayload.read(0, 1, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 100); - - assertEquals(before, after); - } - - @Test - public void ensureMessageCanBeDecrypted() throws IOException, DecryptionFailedException { - PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); - BitmessageAddress identity = new BitmessageAddress(privateKey); - assertEquals("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8", identity.getAddress()); - - ObjectMessage object = TestUtils.loadObjectMessage(3, "V1Msg.payload"); - Msg msg = (Msg) object.getPayload(); - msg.decrypt(privateKey.getPrivateEncryptionKey()); - Plaintext plaintext = msg.getPlaintext(); - assertNotNull(plaintext); - assertEquals("Test", plaintext.getSubject()); - assertEquals("Hallo, das ist ein Test von der v4-Adresse", plaintext.getText()); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java deleted file mode 100644 index 039e7ad..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage; - -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.Msg; -import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.TestUtils; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Arrays; - -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.*; - -/** - * @author Christian Basler - */ -public class ProofOfWorkServiceTest { - private ProofOfWorkService proofOfWorkService; - - private Cryptography cryptography; - @Mock - private InternalContext ctx; - @Mock - private ProofOfWorkRepository proofOfWorkRepo; - @Mock - private Inventory inventory; - @Mock - private NetworkHandler networkHandler; - @Mock - private MessageRepository messageRepo; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - cryptography = spy(new BouncyCryptography()); - Singleton.initialize(cryptography); - - ctx = mock(InternalContext.class); - when(ctx.getProofOfWorkRepository()).thenReturn(proofOfWorkRepo); - when(ctx.getInventory()).thenReturn(inventory); - when(ctx.getNetworkHandler()).thenReturn(networkHandler); - when(ctx.getMessageRepository()).thenReturn(messageRepo); - when(ctx.getLabeler()).thenReturn(mock(Labeler.class)); - when(ctx.getNetworkListener()).thenReturn(mock(NetworkHandler.MessageListener.class)); - - proofOfWorkService = new ProofOfWorkService(); - proofOfWorkService.setContext(ctx); - } - - @Test - public void ensureMissingProofOfWorkIsDone() { - when(proofOfWorkRepo.getItems()).thenReturn(Arrays.asList(new byte[64])); - when(proofOfWorkRepo.getItem(any(byte[].class))).thenReturn(new ProofOfWorkRepository.Item(null, 1001, 1002)); - doNothing().when(cryptography).doProofOfWork(any(ObjectMessage.class), anyLong(), anyLong(), any(ProofOfWorkEngine.Callback.class)); - - proofOfWorkService.doMissingProofOfWork(10); - - verify(cryptography, timeout(1000)).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L), - any(ProofOfWorkEngine.Callback.class)); - } - - @Test - public void ensureCalculatedNonceIsStored() throws Exception { - BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - BitmessageAddress address = TestUtils.loadContact(); - Plaintext plaintext = new Plaintext.Builder(MSG).from(identity).to(address).message("", "").build(); - ObjectMessage object = new ObjectMessage.Builder() - .payload(new Msg(plaintext)) - .build(); - object.sign(identity.getPrivateKey()); - object.encrypt(address.getPubkey()); - byte[] initialHash = new byte[64]; - byte[] nonce = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; - - when(proofOfWorkRepo.getItem(initialHash)).thenReturn(new ProofOfWorkRepository.Item(object, 1001, 1002)); - when(messageRepo.getMessage(initialHash)).thenReturn(plaintext); - - proofOfWorkService.onNonceCalculated(initialHash, nonce); - - verify(proofOfWorkRepo).removeObject(eq(initialHash)); - verify(inventory).storeObject(eq(object)); - verify(networkHandler).offer(eq(object.getInventoryVector())); - assertThat(plaintext.getInventoryVector(), equalTo(object.getInventoryVector())); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java deleted file mode 100644 index 3566a5f..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java +++ /dev/null @@ -1,69 +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.Msg; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.entity.payload.Pubkey; -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; - -import java.io.IOException; - -import static org.junit.Assert.*; - -public class SignatureTest extends TestBase { - @Test - public void ensureValidationWorks() throws IOException { - ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); - Pubkey pubkey = (Pubkey) object.getPayload(); - assertTrue(object.isSignatureValid(pubkey)); - } - - @Test - public void ensureSigningWorks() throws IOException { - PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); - - ObjectMessage objectMessage = new ObjectMessage.Builder() - .objectType(ObjectType.PUBKEY) - .stream(1) - .payload(privateKey.getPubkey()) - .build(); - objectMessage.sign(privateKey); - - assertTrue(objectMessage.isSignatureValid(privateKey.getPubkey())); - } - - @Test - public void ensureMessageIsProperlySigned() throws IOException, DecryptionFailedException { - BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - - ObjectMessage object = TestUtils.loadObjectMessage(3, "V1Msg.payload"); - Msg msg = (Msg) object.getPayload(); - msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey()); - Plaintext plaintext = msg.getPlaintext(); - assertEquals(TestUtils.loadContact().getPubkey(), plaintext.getFrom().getPubkey()); - assertNotNull(plaintext); - assertTrue(object.isSignatureValid(plaintext.getFrom().getPubkey())); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java deleted file mode 100644 index fced84f..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java +++ /dev/null @@ -1,160 +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.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.exception.DecryptionFailedException; -import ch.dissem.bitmessage.utils.*; -import org.junit.Test; - -import java.io.IOException; -import java.util.Arrays; - -import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; -import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.INCLUDE_DESTINATION; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static org.junit.Assert.*; - -public class BitmessageAddressTest extends TestBase { - @Test - public void ensureFeatureFlagIsCalculatedCorrectly() { - assertEquals(1, Pubkey.Feature.bitfield(DOES_ACK)); - assertEquals(2, Pubkey.Feature.bitfield(INCLUDE_DESTINATION)); - assertEquals(3, Pubkey.Feature.bitfield(DOES_ACK, INCLUDE_DESTINATION)); - } - - @Test - public void ensureBase58DecodesCorrectly() { - assertHexEquals("800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D", - Base58.decode("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ")); - } - - @Test - public void ensureAddressStaysSame() { - String address = "BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"; - assertEquals(address, new BitmessageAddress(address).toString()); - } - - @Test - public void ensureStreamAndVersionAreParsed() { - BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - assertEquals(1, address.getStream()); - assertEquals(3, address.getVersion()); - - address = new BitmessageAddress("BM-87hJ99tPAXxtetvnje7Z491YSvbEtBJVc5e"); - assertEquals(1, address.getStream()); - assertEquals(4, address.getVersion()); - } - - @Test - public void ensureIdentityCanBeCreated() { - BitmessageAddress address = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); - assertNotNull(address.getPubkey()); - assertTrue(address.has(DOES_ACK)); - } - - @Test - public void ensureV2PubkeyCanBeImported() throws IOException { - ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); - Pubkey pubkey = (Pubkey) object.getPayload(); - BitmessageAddress address = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - try { - address.setPubkey(pubkey); - } catch (Exception e) { - fail(e.getMessage()); - } - } - - @Test - public void ensureV3PubkeyCanBeImported() throws IOException { - BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe()); - - ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); - Pubkey pubkey = (Pubkey) object.getPayload(); - assertTrue(object.isSignatureValid(pubkey)); - try { - address.setPubkey(pubkey); - } catch (Exception e) { - fail(e.getMessage()); - } - - assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); - assertTrue(address.has(DOES_ACK)); - } - - @Test - public void ensureV4PubkeyCanBeImported() throws IOException, DecryptionFailedException { - BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload"); - object.decrypt(address.getPublicDecryptionKey()); - V4Pubkey pubkey = (V4Pubkey) object.getPayload(); - assertTrue(object.isSignatureValid(pubkey)); - try { - address.setPubkey(pubkey); - } catch (Exception e) { - fail(e.getMessage()); - } - assertTrue(address.has(DOES_ACK)); - } - - @Test - public void ensureV3IdentityCanBeImported() throws IOException { - String address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"; - assertEquals(3, new BitmessageAddress(address_string).getVersion()); - assertEquals(1, new BitmessageAddress(address_string).getStream()); - - byte[] privsigningkey = getSecret("5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9"); - byte[] privencryptionkey = getSecret("5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck"); - - System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n"); - - BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - cryptography().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))); - assertEquals(address_string, address.getAddress()); - } - - @Test - public void ensureV4IdentityCanBeImported() throws IOException { - assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion()); - byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); - byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); - BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - cryptography().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); - assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); - } - - private void assertHexEquals(String hex, byte[] bytes) { - assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase()); - } - - private byte[] getSecret(String walletImportFormat) throws IOException { - byte[] bytes = Base58.decode(walletImportFormat); - if (bytes[0] != (byte) 0x80) - throw new IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + " was " + bytes[0]); - if (bytes.length != 37) - throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); - - byte[] hash = cryptography().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); - } - return Arrays.copyOfRange(bytes, 1, 33); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java deleted file mode 100644 index c417df1..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package ch.dissem.bitmessage.entity; - -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; -import ch.dissem.bitmessage.entity.valueobject.extended.Message; -import ch.dissem.bitmessage.entity.valueobject.extended.Vote; -import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; -import ch.dissem.bitmessage.utils.Bytes; -import ch.dissem.bitmessage.utils.TestUtils; -import org.junit.Test; - -import java.io.UnsupportedEncodingException; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; - -/** - * @author Christian Basler - */ -public class ExtendedEncodingTest { - private ExtendedEncodingFactory extendedEncodingFactory = ExtendedEncodingFactory.getInstance(); - - @Test - public void ensureSimpleMessageIsDecoded() { - ExtendedEncoding extended = extendedEncodingFactory.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")); - assertThat(extended, instanceOf(ExtendedEncoding.class)); - assertThat(extended.getContent(), instanceOf(Message.class)); - assertThat(((Message) extended.getContent()).getSubject(), is("Extended encoding on Windows works - but how ??")); - assertThat(((Message) extended.getContent()).getBody(), notNullValue()); - assertThat(((Message) extended.getContent()).getBody().length(), is(6233)); - } - - - @Test - public void ensureSimpleMessageIsEncoded() { - ExtendedEncoding in = new Message.Builder() - .subject("Test sübject") - .body("test bödy") - .build(); - - assertThat(in.zip(), notNullValue()); - - ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); - assertThat(out, is(in)); - } - - @Test - public void ensureCompleteMessageIsEncodedAndDecoded() throws UnsupportedEncodingException { - ExtendedEncoding in = new Message.Builder() - .addParent(TestUtils.randomInventoryVector()) - .addParent(TestUtils.randomInventoryVector()) - .subject("Test sübject") - .body("test bödy") - .addFile( - new Attachment.Builder() - .name("test.txt") - .type("text/plain") - .data("test".getBytes("UTF-8")) - .attachment() - .build() - ) - .build(); - - assertThat(in.zip(), notNullValue()); - - ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); - assertThat(out, is(in)); - } - - @Test - public void ensureVoteIsEncodedAndDecoded() { - ExtendedEncoding in = new Vote.Builder() - .msgId(TestUtils.randomInventoryVector()) - .vote("+1") - .build(); - - assertThat(in.zip(), notNullValue()); - - ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); - assertThat(out, is(in)); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java deleted file mode 100644 index 3e96b77..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ /dev/null @@ -1,198 +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.entity; - -import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.entity.valueobject.extended.Message; -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.*; -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.hamcrest.Matchers.is; -import static org.junit.Assert.*; - -public class SerializationTest extends TestBase { - @Test - public void ensureGetPubkeyIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V2GetPubkey.payload", 2, GetPubkey.class); - doTest("V3GetPubkey.payload", 2, GetPubkey.class); - doTest("V4GetPubkey.payload", 2, GetPubkey.class); - } - - @Test - public void ensureV2PubkeyIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V2Pubkey.payload", 2, V2Pubkey.class); - } - - @Test - public void ensureV3PubkeyIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V3Pubkey.payload", 3, V3Pubkey.class); - } - - @Test - public void ensureV4PubkeyIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V4Pubkey.payload", 4, V4Pubkey.class); - } - - @Test - public void ensureV1MsgIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V1Msg.payload", 1, Msg.class); - } - - @Test - public void ensureV4BroadcastIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V4Broadcast.payload", 4, V4Broadcast.class); - } - - @Test - public void ensureV5BroadcastIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V5Broadcast.payload", 5, V5Broadcast.class); - } - - @Test - public void ensureUnknownDataIsDeserializedAndSerializedCorrectly() throws IOException { - doTest("V1MsgStrangeData.payload", 1, GenericPayload.class); - } - - @Test - public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws Exception { - Plaintext expected = new Plaintext.Builder(MSG) - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .message("Subject", "Message") - .ackData("ackMessage".getBytes()) - .signature(new byte[0]) - .build(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - expected.write(out); - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - Plaintext actual = Plaintext.read(MSG, in); - - // Received is automatically set on deserialization, so we'll need to set it to null - Field received = Plaintext.class.getDeclaredField("received"); - received.setAccessible(true); - received.set(actual, null); - - assertThat(expected, is(actual)); - } - - @Test - public void ensurePlaintextWithExtendedEncodingIsSerializedAndDeserializedCorrectly() throws Exception { - Plaintext expected = new Plaintext.Builder(MSG) - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .message(new Message.Builder() - .subject("Subject") - .body("Message") - .build()) - .ackData("ackMessage".getBytes()) - .signature(new byte[0]) - .build(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - expected.write(out); - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - Plaintext actual = Plaintext.read(MSG, in); - - // Received is automatically set on deserialization, so we'll need to set it to null - Field received = Plaintext.class.getDeclaredField("received"); - received.setAccessible(true); - received.set(actual, null); - - assertEquals(expected, actual); - } - - @Test - public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception { - Plaintext expected = new Plaintext.Builder(MSG) - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .message("Subject", "Message") - .ackData("ackMessage".getBytes()) - .signature(new byte[0]) - .build(); - ObjectMessage ackMessage1 = expected.getAckMessage(); - assertNotNull(ackMessage1); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - expected.write(out); - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - Plaintext actual = Plaintext.read(MSG, in); - - // Received is automatically set on deserialization, so we'll need to set it to null - Field received = Plaintext.class.getDeclaredField("received"); - received.setAccessible(true); - received.set(actual, null); - - assertEquals(expected, actual); - assertEquals(ackMessage1, actual.getAckMessage()); - } - - @Test - public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { - ArrayList<InventoryVector> ivs = new ArrayList<>(50000); - for (int i = 0; i < 50000; i++) { - ivs.add(TestUtils.randomInventoryVector()); - } - - Inv inv = new Inv.Builder().inventory(ivs).build(); - NetworkMessage before = new NetworkMessage(inv); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - before.write(out); - - NetworkMessage after = Factory.getNetworkMessage(3, new ByteArrayInputStream(out.toByteArray())); - assertNotNull(after); - Inv invAfter = (Inv) after.getPayload(); - assertEquals(ivs, invAfter.getInventory()); - } - - private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException { - byte[] data = TestUtils.getBytes(resourceName); - InputStream in = new ByteArrayInputStream(data); - 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 deleted file mode 100644 index c2efb1f..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java +++ /dev/null @@ -1,72 +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.CallbackWaiter; -import ch.dissem.bitmessage.utils.TestBase; -import org.junit.Test; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -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 = cryptography().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(cryptography().doubleSha512(nonce, initialHash), target, 8)); - - // Let's add a second (shorter) run to find possible multi threading issues - byte[] initialHash2 = cryptography().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(cryptography().doubleSha512(nonce2, initialHash2), target2, 8)); - assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime()); - } - -} diff --git a/core/src/test/java/ch/dissem/bitmessage/testutils/TestInventory.java b/core/src/test/java/ch/dissem/bitmessage/testutils/TestInventory.java deleted file mode 100644 index 1f8cfd3..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/testutils/TestInventory.java +++ /dev/null @@ -1,81 +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.testutils; - -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/core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java deleted file mode 100644 index 1af8d37..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/utils/BytesTest.java +++ /dev/null @@ -1,97 +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 org.junit.Ignore; -import org.junit.Test; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.Random; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -public class BytesTest { - public static final Random rnd = new Random(); - - @Test - public void ensureExpandsCorrectly() { - byte[] source = {1}; - byte[] expected = {0, 1}; - assertArrayEquals(expected, Bytes.expand(source, 2)); - } - - @Test - public void ensureIncrementCarryWorks() throws IOException { - byte[] bytes = {0, -1}; - Bytes.inc(bytes); - assertArrayEquals(TestUtils.int16(256), bytes); - } - - @Test - public void testIncrementByValue() throws IOException { - for (int v = 0; v < 256; v++) { - for (int i = 1; i < 256; i++) { - byte[] bytes = {0, (byte) v}; - Bytes.inc(bytes, (byte) i); - assertArrayEquals("value = " + v + "; inc = " + i + "; expected = " + (v + i), TestUtils.int16(v + i), bytes); - } - } - } - - /** - * 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++) { - BigInteger a = BigInteger.valueOf(rnd.nextLong()).pow((rnd.nextInt(5) + 1)).abs(); - BigInteger b = BigInteger.valueOf(rnd.nextLong()).pow((rnd.nextInt(5) + 1)).abs(); - System.out.println("a = " + a.toString(16) + "\tb = " + b.toString(16)); - assertEquals(a.compareTo(b) == -1, Bytes.lt(a.toByteArray(), b.toByteArray())); - } - } - - @Test - public void testLowerThanBounded() { - for (int i = 0; i < 1000; i++) { - BigInteger a = BigInteger.valueOf(rnd.nextLong()).pow((rnd.nextInt(5) + 1)).abs(); - BigInteger b = BigInteger.valueOf(rnd.nextLong()).pow((rnd.nextInt(5) + 1)).abs(); - System.out.println("a = " + a.toString(16) + "\tb = " + b.toString(16)); - assertEquals(a.compareTo(b) == -1, Bytes.lt( - Bytes.expand(a.toByteArray(), 100), - Bytes.expand(b.toByteArray(), 100), - 100)); - } - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java deleted file mode 100644 index 6543232..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2017 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; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.extended.Message; -import ch.dissem.bitmessage.ports.MessageRepository; -import org.junit.Test; - -import java.util.LinkedList; -import java.util.List; -import java.util.UUID; - -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.TestUtils.RANDOM; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ConversationServiceTest { - private BitmessageAddress alice = new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - private BitmessageAddress bob = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj"); - - private MessageRepository messageRepository = mock(MessageRepository.class); - private ConversationService conversationService = new ConversationService(messageRepository); - - static { - Singleton.initialize(new BouncyCryptography()); - } - - @Test - public void ensureConversationIsSortedProperly() { - List<Plaintext> expected = getConversation(); - - when(conversationService.getConversation(any(UUID.class))).thenReturn(expected); - List<Plaintext> actual = conversationService.getConversation(UUID.randomUUID()); - assertThat(actual, is(expected)); - } - - private List<Plaintext> getConversation() { - List<Plaintext> result = new LinkedList<>(); - - Plaintext older = plaintext(alice, bob, - new Message.Builder() - .subject("hey there") - .body("does it work?") - .build(), - Plaintext.Status.SENT); - result.add(older); - - Plaintext root = plaintext(alice, bob, - new Message.Builder() - .subject("new test") - .body("There's a new test in town!") - .build(), - Plaintext.Status.SENT); - result.add(root); - - result.add( - plaintext(bob, alice, - new Message.Builder() - .subject("Re: new test (1a)") - .body("Nice!") - .addParent(root) - .build(), - Plaintext.Status.RECEIVED) - ); - - Plaintext latest = plaintext(bob, alice, - new Message.Builder() - .subject("Re: new test (2b)") - .body("PS: it did work!") - .addParent(root) - .addParent(older) - .build(), - Plaintext.Status.RECEIVED); - result.add(latest); - - result.add( - plaintext(alice, bob, - new Message.Builder() - .subject("Re: new test (2)") - .body("") - .addParent(latest) - .build(), - Plaintext.Status.DRAFT) - ); - - return result; - } - - private int timer = 2; - - private Plaintext plaintext(BitmessageAddress from, BitmessageAddress to, - ExtendedEncoding content, Plaintext.Status status) { - Plaintext.Builder builder = new Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(from) - .to(to) - .message(content) - .status(status); - if (status != Plaintext.Status.DRAFT && status != Plaintext.Status.DOING_PROOF_OF_WORK) { - builder.received(5L * ++timer - RANDOM.nextInt(10)); - } - return builder.build(); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java deleted file mode 100644 index 23d602c..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java +++ /dev/null @@ -1,124 +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 org.junit.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -public class EncodeTest { - @Test - public void testUint8() throws IOException { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Encode.int8(0, stream); - checkBytes(stream, 0); - - stream = new ByteArrayOutputStream(); - Encode.int8(255, stream); - checkBytes(stream, 255); - } - - @Test - public void testUint16() throws IOException { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Encode.int16(0, stream); - checkBytes(stream, 0, 0); - - stream = new ByteArrayOutputStream(); - Encode.int16(513, stream); - checkBytes(stream, 2, 1); - } - - @Test - public void testUint32() throws IOException { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Encode.int32(0, stream); - checkBytes(stream, 0, 0, 0, 0); - - stream = new ByteArrayOutputStream(); - Encode.int32(67305985, stream); - checkBytes(stream, 4, 3, 2, 1); - - stream = new ByteArrayOutputStream(); - Encode.int32(3355443201L, stream); - checkBytes(stream, 200, 0, 0, 1); - } - - @Test - public void testUint64() throws IOException { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Encode.int64(0, stream); - checkBytes(stream, 0, 0, 0, 0, 0, 0, 0, 0); - - stream = new ByteArrayOutputStream(); - Encode.int64(578437695752307201L, stream); - checkBytes(stream, 8, 7, 6, 5, 4, 3, 2, 1); - - stream = new ByteArrayOutputStream(); - // 200 * 72057594037927936L + 1 - Encode.int64(0xc800000000000001L, stream); - checkBytes(stream, 200, 0, 0, 0, 0, 0, 0, 1); - } - - @Test - public void testVarInt() throws IOException { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Encode.varInt(0, stream); - checkBytes(stream, 0); - - stream = new ByteArrayOutputStream(); - Encode.varInt(252, stream); - checkBytes(stream, 252); - - stream = new ByteArrayOutputStream(); - Encode.varInt(253, stream); - checkBytes(stream, 253, 0, 253); - - stream = new ByteArrayOutputStream(); - Encode.varInt(65535, stream); - checkBytes(stream, 253, 255, 255); - - stream = new ByteArrayOutputStream(); - Encode.varInt(65536, stream); - checkBytes(stream, 254, 0, 1, 0, 0); - - stream = new ByteArrayOutputStream(); - Encode.varInt(4294967295L, stream); - checkBytes(stream, 254, 255, 255, 255, 255); - - stream = new ByteArrayOutputStream(); - Encode.varInt(4294967296L, stream); - checkBytes(stream, 255, 0, 0, 0, 1, 0, 0, 0, 0); - - stream = new ByteArrayOutputStream(); - Encode.varInt(-1L, stream); - checkBytes(stream, 255, 255, 255, 255, 255, 255, 255, 255, 255); - } - - - public void checkBytes(ByteArrayOutputStream stream, int... bytes) { - assertEquals(bytes.length, stream.size()); - byte[] streamBytes = stream.toByteArray(); - - for (int i = 0; i < bytes.length; i++) { - assertEquals((byte) bytes[i], streamBytes[i]); - } - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java deleted file mode 100644 index 5be73ff..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.utils; - -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import org.mockito.ArgumentMatcher; -import org.mockito.ArgumentMatchers; - -/** - * @author Christian Basler - */ -public class MessageMatchers { - public static Plaintext plaintext(final Plaintext.Type type) { - return ArgumentMatchers.argThat(new ArgumentMatcher<Plaintext>() { - @Override - public boolean matches(Plaintext item) { - return item != null && item.getType() == type; - } - }); - } - - public static ObjectMessage object(final ObjectType type) { - return ArgumentMatchers.argThat(new ArgumentMatcher<ObjectMessage>() { - @Override - public boolean matches(ObjectMessage item) { - return item != null && item.getPayload().getType() == type; - } - }); - } -} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java deleted file mode 100644 index 6fadce4..0000000 --- a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java +++ /dev/null @@ -1,96 +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.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.payload.V4Pubkey; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.factory.Factory; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Random; - -import static org.junit.Assert.assertEquals; - -/** - * If there's ever a need for this in production code, it should be rewritten to be more efficient. - */ -public class TestUtils { - public static final Random RANDOM = new Random(); - - public static byte[] int16(int number) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Encode.int16(number, out); - return out.toByteArray(); - } - - public static ObjectMessage loadObjectMessage(int version, String resourceName) throws IOException { - byte[] data = getBytes(resourceName); - InputStream in = new ByteArrayInputStream(data); - return Factory.getObjectMessage(version, in, data.length); - } - - public static byte[] getBytes(String resourceName) throws IOException { - InputStream in = TestUtils.class.getClassLoader().getResourceAsStream(resourceName); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int len = in.read(buffer); - while (len != -1) { - out.write(buffer, 0, len); - len = in.read(buffer); - } - return out.toByteArray(); - } - - public static InventoryVector randomInventoryVector() { - byte[] bytes = new byte[32]; - RANDOM.nextBytes(bytes); - return InventoryVector.fromHash(bytes); - } - - public static InputStream getResource(String resourceName) { - return TestUtils.class.getClassLoader().getResourceAsStream(resourceName); - } - - public static BitmessageAddress loadIdentity(String address) throws IOException { - PrivateKey privateKey = PrivateKey.read(TestUtils.getResource(address + ".privkey")); - BitmessageAddress identity = new BitmessageAddress(privateKey); - assertEquals(address, identity.getAddress()); - return identity; - } - - public static BitmessageAddress loadContact() throws IOException, DecryptionFailedException { - BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - ObjectMessage object = TestUtils.loadObjectMessage(3, "V4Pubkey.payload"); - object.decrypt(address.getPublicDecryptionKey()); - address.setPubkey((V4Pubkey) object.getPayload()); - return address; - } - - public static void loadPubkey(BitmessageAddress address) throws IOException { - byte[] bytes = getBytes(address.getAddress() + ".pubkey"); - Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(), new ByteArrayInputStream(bytes), bytes.length, false); - address.setPubkey(pubkey); - } -} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt new file mode 100644 index 0000000..24e8ae3 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt @@ -0,0 +1,330 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.ports.DefaultLabeler +import ch.dissem.bitmessage.ports.ProofOfWorkEngine +import ch.dissem.bitmessage.ports.ProofOfWorkRepository +import ch.dissem.bitmessage.testutils.TestInventory +import ch.dissem.bitmessage.utils.Property +import ch.dissem.bitmessage.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.Strings.hex +import ch.dissem.bitmessage.utils.TTL +import ch.dissem.bitmessage.utils.TestUtils +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import com.nhaarman.mockito_kotlin.* +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.notNullValue +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.util.* +import kotlin.concurrent.thread + +/** + * @author Christian Basler + */ +class BitmessageContextTest { + private var listener: BitmessageContext.Listener = mock() + private val inventory = spy(TestInventory()) + private val testPowRepo = spy(object : ProofOfWorkRepository { + internal var items: MutableMap<InventoryVector, ProofOfWorkRepository.Item> = HashMap() + internal var added = 0 + internal var removed = 0 + + override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item { + return items[InventoryVector(initialHash)] ?: throw IllegalArgumentException("${hex(initialHash)} not found in $items") + } + + override fun getItems(): List<ByteArray> { + val result = LinkedList<ByteArray>() + for ((hash) in items.keys) { + result.add(hash) + } + return result + } + + override fun putObject(item: ProofOfWorkRepository.Item) { + items.put(InventoryVector(cryptography().getInitialHash(item.objectMessage)), item) + added++ + } + + override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) { + items.put(InventoryVector(cryptography().getInitialHash(objectMessage)), ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes)) + added++ + } + + override fun removeObject(initialHash: ByteArray) { + if (items.remove(InventoryVector(initialHash)) != null) { + removed++ + } + } + + fun reset() { + items.clear() + added = 0 + removed = 0 + } + }) + private val testPowEngine = spy(object : ProofOfWorkEngine { + override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) { + thread { callback.onNonceCalculated(initialHash, ByteArray(8)) } + } + }) + private var ctx = BitmessageContext.Builder() + .addressRepo(mock()) + .cryptography(BouncyCryptography()) + .inventory(inventory) + .listener(listener) + .messageRepo(mock()) + .networkHandler(mock { + on { getNetworkStatus() } doReturn Property("test", "mocked") + }) + .nodeRegistry(mock()) + .labeler(spy(DefaultLabeler())) + .powRepo(testPowRepo) + .proofOfWorkEngine(testPowEngine) + .build() + + init { + TTL.msg = 2 * MINUTE + } + + @Before + fun setUp() { + testPowRepo.reset() + } + + @Test + fun `ensure contact is saved and pubkey requested`() { + val contact = BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT") + doReturn(contact).whenever(ctx.addresses).getAddress(eq(contact.address)) + + ctx.addContact(contact) + + verify(ctx.addresses, timeout(1000).atLeastOnce()).save(eq(contact)) + verify(testPowEngine, timeout(1000)).calculateNonce(any(), any(), any()) + } + + @Test + fun `ensure pubkey is not requested if it exists`() { + val (_, _, payload) = TestUtils.loadObjectMessage(2, "V2Pubkey.payload") + val pubkey = payload as Pubkey + val contact = BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT") + contact.pubkey = pubkey + + ctx.addContact(contact) + + verify(ctx.addresses, times(1)).save(contact) + verify(testPowEngine, never()).calculateNonce(any(), any(), any()) + } + + @Test + fun `ensure V2Pubkey is not requested if it exists in inventory`() { + inventory.init( + "V1Msg.payload", + "V2GetPubkey.payload", + "V2Pubkey.payload", + "V3GetPubkey.payload", + "V3Pubkey.payload", + "V4Broadcast.payload", + "V4GetPubkey.payload", + "V4Pubkey.payload", + "V5Broadcast.payload" + ) + val contact = BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT") + + whenever(ctx.addresses.getAddress(contact.address)).thenReturn(contact) + + ctx.addContact(contact) + + verify(ctx.addresses, atLeastOnce()).save(contact) + verify(testPowEngine, never()).calculateNonce(any(), any(), any()) + } + + @Test + fun `ensure V4Pubkey is not requested if it exists in inventory`() { + inventory.init( + "V1Msg.payload", + "V2GetPubkey.payload", + "V2Pubkey.payload", + "V3GetPubkey.payload", + "V3Pubkey.payload", + "V4Broadcast.payload", + "V4GetPubkey.payload", + "V4Pubkey.payload", + "V5Broadcast.payload" + ) + val contact = BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + val stored = BitmessageAddress(contact.address) + stored.alias = "Test" + whenever(ctx.addresses.getAddress(contact.address)).thenReturn(stored) + + ctx.addContact(contact) + + verify(ctx.addresses, atLeastOnce()).save(any()) + verify(testPowEngine, never()).calculateNonce(any(), any(), any()) + } + + @Test + fun `ensure subscription is added and existing broadcasts retrieved`() { + val address = BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + + inventory.init( + "V4Broadcast.payload", + "V5Broadcast.payload" + ) + + whenever(ctx.addresses.getSubscriptions(any())).thenReturn(listOf(address)) + ctx.addSubscribtion(address) + + verify(ctx.addresses, atLeastOnce()).save(address) + assertThat(address.isSubscribed, `is`(true)) + verify(ctx.internals.inventory).getObjects(eq(address.stream), any(), any()) + verify(listener).receive(any()) + } + + @Test + fun `ensure identity is created`() { + assertThat(ctx.createIdentity(false), notNullValue()) + } + + @Test + fun `ensure message is sent`() { + ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(), + "Subject", "Message") + verify(ctx.internals.proofOfWorkRepository, timeout(10000)).putObject( + argThat { payload.type == ObjectType.MSG }, eq(1000L), eq(1000L)) + assertEquals(2, testPowRepo.added) + verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG }) + } + + @Test + fun `ensure pubkey is requested if it is missing`() { + ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), + BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + "Subject", "Message") + verify(testPowRepo, timeout(10000).atLeastOnce()) + .putObject(argThat { payload.type == ObjectType.GET_PUBKEY }, eq(1000L), eq(1000L)) + verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG }) + } + + @Test(expected = IllegalArgumentException::class) + fun `ensure sender must be identity`() { + ctx.send(BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + "Subject", "Message") + } + + @Test + fun `ensure broadcast is sent`() { + ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), + "Subject", "Message") + verify(ctx.internals.proofOfWorkRepository, timeout(1000).atLeastOnce()) + .putObject(argThat { payload.type == ObjectType.BROADCAST }, eq(1000L), eq(1000L)) + verify(testPowEngine).calculateNonce(any(), any(), any()) + verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.BROADCAST }) + } + + @Test(expected = IllegalArgumentException::class) + fun `ensure sender without private key throws exception`() { + val msg = Plaintext.Builder(Type.BROADCAST) + .from(BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .message("Subject", "Message") + .build() + ctx.send(msg) + } + + @Test + fun `ensure chan is joined`() { + val chanAddress = "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r" + val chan = ctx.joinChan("general", chanAddress) + assertNotNull(chan) + assertEquals(chan.address, chanAddress) + assertTrue(chan.isChan) + } + + @Test + fun `ensure deterministic addresses are created`() { + val expected_size = 8 + val addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, false) + assertEquals(expected_size, addresses.size) + val expected = HashSet<String>(expected_size) + expected.add("BM-2cWFkyuXXFw6d393RGnin2RpSXj8wxtt6F") + expected.add("BM-2cX8TF9vuQZEWvT7UrEeq1HN9dgiSUPLEN") + expected.add("BM-2cUzX8f9CKUU7L8NeB8GExZvf54PrcXq1S") + expected.add("BM-2cU7MAoQd7KE8SPF7AKFPpoEZKjk86KRqE") + expected.add("BM-2cVm8ByVBacc2DVhdTNs6rmy5ZQK6DUsrt") + expected.add("BM-2cW2af1vB6kWon2WkygDHqGwfcpfAFm2Jk") + expected.add("BM-2cWdWD7UtUN4gWChgNX9pvyvNPjUZvU8BT") + expected.add("BM-2cXkYgYcUrv4fGxSHzyEScW955Cc8sDteo") + for (a in addresses) { + assertTrue(expected.contains(a.address)) + expected.remove(a.address) + } + } + + @Test + fun `ensure short deterministic addresses are created`() { + val expected_size = 1 + val addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, true) + assertEquals(expected_size, addresses.size) + val expected = HashSet<String>(expected_size) + expected.add("BM-NBGyBAEp6VnBkFWKpzUSgxuTqVdWPi78") + for (a in addresses) { + assertTrue(expected.contains(a.address)) + expected.remove(a.address) + } + } + + @Test + fun `ensure chan is created`() { + val chan = ctx.createChan("test") + assertNotNull(chan) + assertEquals(chan.version, Pubkey.LATEST_VERSION) + assertTrue(chan.isChan) + } + + @Test + fun `ensure unacknowledged message is resent`() { + val plaintext = Plaintext.Builder(Type.MSG) + .ttl(1) + .message("subject", "message") + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .build() + assertTrue(plaintext.to!!.has(Pubkey.Feature.DOES_ACK)) + whenever(ctx.messages.findMessagesToResend()).thenReturn(listOf(plaintext)) + whenever(ctx.messages.getMessage(any<ByteArray>())).thenReturn(plaintext) + ctx.resendUnacknowledgedMessages() + verify(ctx.labeler, timeout(1000).times(1)).markAsSent(eq(plaintext)) + } + + @Test + fun `ensure status contains user agent`() { + val userAgent = ctx.status().getProperty("user agent")?.value.toString() + assertThat(userAgent, `is`("/Jabit:${BitmessageContext.version}/")) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/DecryptionTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/DecryptionTest.kt new file mode 100644 index 0000000..dd9bc95 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/DecryptionTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2017 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.payload.V4Broadcast +import ch.dissem.bitmessage.entity.payload.V5Broadcast +import ch.dissem.bitmessage.utils.TestBase +import ch.dissem.bitmessage.utils.TestUtils +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class DecryptionTest : TestBase() { + @Test + fun `ensure V4Broadcast is decrypted correctly`() { + val address = BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + TestUtils.loadPubkey(address) + val objectMessage = TestUtils.loadObjectMessage(5, "V4Broadcast.payload") + val broadcast = objectMessage.payload as V4Broadcast + broadcast.decrypt(address) + assertEquals("Test-Broadcast", broadcast.plaintext?.subject) + assertTrue(objectMessage.isSignatureValid(address.pubkey!!)) + } + + @Test + fun `ensure V5Broadcast is decrypted correctly`() { + val address = BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + TestUtils.loadPubkey(address) + val objectMessage = TestUtils.loadObjectMessage(5, "V5Broadcast.payload") + val broadcast = objectMessage.payload as V5Broadcast + broadcast.decrypt(address) + assertEquals("Test-Broadcast", broadcast.plaintext?.subject) + assertTrue(objectMessage.isSignatureValid(address.pubkey!!)) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt new file mode 100644 index 0000000..a96430f --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED +import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.GetPubkey +import ch.dissem.bitmessage.entity.payload.Msg +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.ports.ProofOfWorkRepository +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TestBase +import ch.dissem.bitmessage.utils.TestUtils +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import ch.dissem.bitmessage.utils.UnixTime.now +import com.nhaarman.mockito_kotlin.* +import org.junit.Before +import org.junit.Test + +/** + * @author Christian Basler + */ +class DefaultMessageListenerTest : TestBase() { + private lateinit var listener: DefaultMessageListener + + private val ctx = TestUtils.mockedInternalContext( + cryptography = BouncyCryptography() + ) + + @Before + fun setUp() { + listener = ctx.networkListener as DefaultMessageListener + } + + @Test + fun `ensure pubkey is sent on request`() { + val identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + whenever(ctx.addressRepository.findIdentity(any())).thenReturn(identity) + val objectMessage = ObjectMessage( + stream = 2, + payload = GetPubkey(BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")), + expiresTime = now + MINUTE + ) + whenever(ctx.proofOfWorkRepository.getItem(any())).thenReturn(ProofOfWorkRepository.Item(objectMessage, 1000L, 1000L)) + listener.receive(objectMessage) + verify(ctx.proofOfWorkRepository).putObject(argThat { type == ObjectType.PUBKEY.number }, any(), any()) + } + + @Test + fun `ensure incoming pubkey is added to contact`() { + val identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val contact = BitmessageAddress(identity.address) + whenever(ctx.addressRepository.findContact(isA())).thenReturn(contact) + whenever(ctx.messageRepository.findMessages(eq(PUBKEY_REQUESTED), eq(contact))) + .thenReturn(listOf(Plaintext.Builder(MSG).from(identity).to(contact).message("S", "T").build())) + + val objectMessage = ObjectMessage.Builder() + .stream(2) + .payload(identity.pubkey!!) + .build() + objectMessage.sign(identity.privateKey!!) + objectMessage.encrypt(Singleton.cryptography().createPublicKey(identity.publicDecryptionKey)) + whenever(ctx.proofOfWorkRepository.getItem(any())).thenReturn(ProofOfWorkRepository.Item(objectMessage, 1000L, 1000L)) + listener.receive(objectMessage) + + verify(ctx.addressRepository).save(eq(contact)) + } + + @Test + fun `ensure incoming message is saved`() { + val identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val contact = BitmessageAddress(identity.address) + contact.pubkey = identity.pubkey + + whenever(ctx.addressRepository.getIdentities()).thenReturn(listOf(identity)) + + val objectMessage = ObjectMessage.Builder() + .stream(2) + .payload(Msg(Plaintext.Builder(MSG) + .from(identity) + .to(contact) + .message("S", "T") + .build())) + .nonce(ByteArray(8)) + .build() + objectMessage.sign(identity.privateKey!!) + objectMessage.encrypt(identity.pubkey!!) + + listener.receive(objectMessage) + + verify(ctx.messageRepository, atLeastOnce()).save(argThat { type == MSG }) + } + + @Test + fun `ensure incoming broadcast is saved`() { + val identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + + whenever(ctx.addressRepository.getSubscriptions(any())).thenReturn(listOf(identity)) + + val broadcast = Factory.getBroadcast(Plaintext.Builder(BROADCAST) + .from(identity) + .message("S", "T") + .build()) + val objectMessage = ObjectMessage.Builder() + .stream(2) + .payload(broadcast) + .nonce(ByteArray(8)) + .build() + objectMessage.sign(identity.privateKey!!) + broadcast.encrypt() + + listener.receive(objectMessage) + + verify(ctx.messageRepository, atLeastOnce()).save(argThat { type == BROADCAST }) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/EncryptionTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/EncryptionTest.kt new file mode 100644 index 0000000..657b468 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/EncryptionTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2017 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.payload.CryptoBox +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.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.TestBase +import ch.dissem.bitmessage.utils.TestUtils +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +class EncryptionTest : TestBase() { + @Test + fun `ensure decrypted data is same as before encryption`() { + val before = GenericPayload(0, 1, cryptography().randomBytes(100)) + + val privateKey = PrivateKey(false, 1, 1000, 1000) + val cryptoBox = CryptoBox(before, privateKey.pubkey.encryptionKey) + + val after = GenericPayload.read(0, 1, cryptoBox.decrypt(privateKey.privateEncryptionKey), 100) + + assertEquals(before, after) + } + + @Test + fun `ensure message can be decrypted`() { + val privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")) + val identity = BitmessageAddress(privateKey) + assertEquals("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8", identity.address) + + val (_, _, payload) = TestUtils.loadObjectMessage(3, "V1Msg.payload") + val msg = payload as Msg + msg.decrypt(privateKey.privateEncryptionKey) + assertNotNull(msg.plaintext) + assertEquals("Test", msg.plaintext?.subject) + assertEquals("Hallo, das ist ein Test von der v4-Adresse", msg.plaintext?.text) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/ProofOfWorkServiceTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/ProofOfWorkServiceTest.kt new file mode 100644 index 0000000..c244ce0 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/ProofOfWorkServiceTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.GenericPayload +import ch.dissem.bitmessage.entity.payload.Msg +import ch.dissem.bitmessage.ports.Cryptography +import ch.dissem.bitmessage.ports.ProofOfWorkRepository +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TestUtils +import com.nhaarman.mockito_kotlin.* +import org.hamcrest.CoreMatchers.equalTo +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import java.util.* +import kotlin.properties.Delegates + +/** + * @author Christian Basler + */ +class ProofOfWorkServiceTest { + private var cryptography by Delegates.notNull<Cryptography>() + private var ctx by Delegates.notNull<InternalContext>() + + private var obj by Delegates.notNull<ObjectMessage>() + + @Before + fun setUp() { + cryptography = spy(BouncyCryptography()) + Singleton.initialize(cryptography) + + ctx = TestUtils.mockedInternalContext( + cryptography = cryptography + ) + + obj = ObjectMessage( + expiresTime = 0, + stream = 1, + payload = GenericPayload(1, 1, kotlin.ByteArray(0)), + type = 42, + version = 42 + ) + } + + @Test + fun `ensure missing proof of work is done`() { + whenever(ctx.proofOfWorkRepository.getItems()).thenReturn(Arrays.asList<ByteArray>(ByteArray(64))) + whenever(ctx.proofOfWorkRepository.getItem(any())).thenReturn(ProofOfWorkRepository.Item(obj, 1001, 1002)) + doNothing().whenever(cryptography).doProofOfWork(any(), any(), any(), any()) + + ctx.proofOfWorkService.doMissingProofOfWork(10) + + verify(cryptography, timeout(1000)).doProofOfWork(eq(obj), eq(1001L), eq(1002L), any()) + } + + @Test + fun `ensure calculated nonce is stored`() { + val identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val address = TestUtils.loadContact() + val plaintext = Plaintext.Builder(MSG).from(identity).to(address).message("", "").build() + val objectMessage = ObjectMessage( + expiresTime = 0, + stream = 1, + payload = Msg(plaintext) + ) + objectMessage.sign(identity.privateKey!!) + objectMessage.encrypt(address.pubkey!!) + val initialHash = ByteArray(64) + val nonce = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8) + + whenever(ctx.proofOfWorkRepository.getItem(initialHash)).thenReturn(ProofOfWorkRepository.Item(objectMessage, 1001, 1002)) + whenever(ctx.messageRepository.getMessage(initialHash)).thenReturn(plaintext) + + ctx.proofOfWorkService.onNonceCalculated(initialHash, nonce) + + verify(ctx.proofOfWorkRepository).removeObject(eq(initialHash)) + verify(ctx.inventory).storeObject(eq(objectMessage)) + verify(ctx.networkHandler).offer(eq(objectMessage.inventoryVector)) + assertThat(plaintext.inventoryVector, equalTo(objectMessage.inventoryVector)) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/SignatureTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/SignatureTest.kt new file mode 100644 index 0000000..a3b965c --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/SignatureTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017 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.ObjectMessage +import ch.dissem.bitmessage.entity.payload.Msg +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.utils.TestBase +import ch.dissem.bitmessage.utils.TestUtils +import org.junit.Assert.* +import org.junit.Test + +class SignatureTest : TestBase() { + @Test + fun `ensure validation works`() { + val objectMessage = TestUtils.loadObjectMessage(3, "V3Pubkey.payload") + val pubkey = objectMessage.payload as Pubkey + assertTrue(objectMessage.isSignatureValid(pubkey)) + } + + @Test + fun `ensure signing works`() { + val privateKey = PrivateKey(false, 1, 1000, 1000) + + val objectMessage = ObjectMessage.Builder() + .objectType(ObjectType.PUBKEY) + .stream(1) + .payload(privateKey.pubkey) + .build() + objectMessage.sign(privateKey) + + assertTrue(objectMessage.isSignatureValid(privateKey.pubkey)) + } + + @Test + fun `ensure message is properly signed`() { + val identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + + val objectMessage = TestUtils.loadObjectMessage(3, "V1Msg.payload") + val msg = objectMessage.payload as Msg + msg.decrypt(identity.privateKey!!.privateEncryptionKey) + assertNotNull(msg.plaintext) + assertEquals(TestUtils.loadContact().pubkey, msg.plaintext!!.from.pubkey) + assertTrue(objectMessage.isSignatureValid(msg.plaintext!!.from.pubkey!!)) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/entity/BitmessageAddressTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/entity/BitmessageAddressTest.kt new file mode 100644 index 0000000..95dcee1 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/entity/BitmessageAddressTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2017 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.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature.INCLUDE_DESTINATION +import ch.dissem.bitmessage.entity.payload.V4Pubkey +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.utils.* +import org.junit.Assert +import org.junit.Assert.* +import org.junit.Test +import java.io.IOException +import java.util.* + +class BitmessageAddressTest : TestBase() { + @Test + fun `ensure feature flag is calculated correctly`() { + Assert.assertEquals(1, Pubkey.Feature.bitfield(DOES_ACK)) + assertEquals(2, Pubkey.Feature.bitfield(INCLUDE_DESTINATION)) + assertEquals(3, Pubkey.Feature.bitfield(DOES_ACK, INCLUDE_DESTINATION)) + } + + @Test + fun `ensure base58 decodes correctly`() { + assertHexEquals("800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D", + Base58.decode("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ")) + } + + @Test + fun `ensure address stays same`() { + val address = "BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ" + assertEquals(address, BitmessageAddress(address).toString()) + } + + @Test + fun `ensure stream and version are parsed`() { + var address = BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + assertEquals(1, address.stream) + assertEquals(3, address.version) + + address = BitmessageAddress("BM-87hJ99tPAXxtetvnje7Z491YSvbEtBJVc5e") + assertEquals(1, address.stream) + assertEquals(4, address.version) + } + + @Test + fun `ensure identity can be created`() { + val address = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + assertNotNull(address.pubkey) + assertTrue(address.has(DOES_ACK)) + } + + @Test + fun `ensure V2Pubkey can be imported`() { + val (_, _, payload) = TestUtils.loadObjectMessage(2, "V2Pubkey.payload") + val pubkey = payload as Pubkey + val address = BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT") + try { + address.pubkey = pubkey + } catch (e: Exception) { + fail(e.message) + } + + } + + @Test + fun `ensure V3Pubkey can be imported`() { + val address = BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + Assert.assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.ripe) + + val objectMessage = TestUtils.loadObjectMessage(3, "V3Pubkey.payload") + val pubkey = objectMessage.payload as Pubkey + assertTrue(objectMessage.isSignatureValid(pubkey)) + try { + address.pubkey = pubkey + } catch (e: Exception) { + fail(e.message) + } + + assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.ripe) + assertTrue(address.has(DOES_ACK)) + } + + @Test + fun `ensure V4Pubkey can be imported`() { + val address = BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + val objectMessage = TestUtils.loadObjectMessage(4, "V4Pubkey.payload") + objectMessage.decrypt(address.publicDecryptionKey) + val pubkey = objectMessage.payload as V4Pubkey + assertTrue(objectMessage.isSignatureValid(pubkey)) + try { + address.pubkey = pubkey + } catch (e: Exception) { + fail(e.message) + } + + assertTrue(address.has(DOES_ACK)) + } + + @Test + fun `ensure V3 identity can be imported`() { + val address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn" + assertEquals(3, BitmessageAddress(address_string).version) + assertEquals(1, BitmessageAddress(address_string).stream) + + val privsigningkey = getSecret("5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9") + val privencryptionkey = getSecret("5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck") + + println("\n\n" + Strings.hex(privsigningkey) + "\n\n") + + val address = BitmessageAddress(PrivateKey(privsigningkey, privencryptionkey, + Singleton.cryptography().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))) + assertEquals(address_string, address.address) + } + + @Test + fun `ensure V4 identity can be imported`() { + assertEquals(4, BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").version) + val privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU") + val privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz") + val address = BitmessageAddress(PrivateKey(privsigningkey, privencryptionkey, + Singleton.cryptography().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))) + assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.address) + } + + private fun assertHexEquals(hex: String, bytes: ByteArray) { + assertEquals(hex.toLowerCase(), Strings.hex(bytes).toLowerCase()) + } + + private fun getSecret(walletImportFormat: String): ByteArray { + val bytes = Base58.decode(walletImportFormat) + if (bytes[0] != 0x80.toByte()) + throw IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + " was " + bytes[0]) + if (bytes.size != 37) + throw IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.size + " long") + + val hash = Singleton.cryptography().doubleSha256(bytes, 33) + for (i in 0..3) { + if (hash[i] != bytes[33 + i]) throw IOException("Hash check failed for secret " + walletImportFormat) + } + return Arrays.copyOfRange(bytes, 1, 33) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/entity/ExtendedEncodingTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/entity/ExtendedEncodingTest.kt new file mode 100644 index 0000000..bc4e8d6 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/entity/ExtendedEncodingTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2017 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.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.entity.valueobject.extended.Vote +import ch.dissem.bitmessage.factory.ExtendedEncodingFactory +import ch.dissem.bitmessage.utils.Bytes +import ch.dissem.bitmessage.utils.TestUtils +import org.hamcrest.Matchers.* +import org.junit.Assert.assertThat +import org.junit.Test + +/** + * @author Christian Basler + */ +class ExtendedEncodingTest { + @Test + fun `ensure simple message is decoded`() { + val extended = ExtendedEncodingFactory.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")) + assertThat<ExtendedEncoding>(extended, instanceOf(ExtendedEncoding::class.java)) + assertThat(extended!!.content, instanceOf<Any>(Message::class.java)) + assertThat((extended.content as Message).subject, `is`("Extended encoding on Windows works - but how ??")) + assertThat((extended.content as Message).body, notNullValue()) + assertThat((extended.content as Message).body.length, `is`(6233)) + } + + + @Test + fun `ensure simple message is encoded`() { + val `in` = Message.Builder() + .subject("Test sübject") + .body("test bödy") + .build() + + assertThat(`in`.zip(), notNullValue()) + + val out = ExtendedEncodingFactory.unzip(`in`.zip()) + assertThat<ExtendedEncoding>(out, `is`(`in`)) + } + + @Test + fun `ensure complete message is encoded and decoded`() { + val `in` = Message.Builder() + .addParent(TestUtils.randomInventoryVector()) + .addParent(TestUtils.randomInventoryVector()) + .subject("Test sübject") + .body("test bödy") + .addFile( + Attachment.Builder() + .name("test.txt") + .type("text/plain") + .data("test".toByteArray(charset("UTF-8"))) + .attachment() + .build() + ) + .build() + + assertThat(`in`.zip(), notNullValue()) + + val out = ExtendedEncodingFactory.unzip(`in`.zip()) + assertThat<ExtendedEncoding>(out, `is`(`in`)) + } + + @Test + fun `ensure vote is encoded and decoded`() { + val `in` = Vote.Builder() + .msgId(TestUtils.randomInventoryVector()) + .vote("+1") + .build() + + assertThat(`in`.zip(), notNullValue()) + + val out = ExtendedEncodingFactory.unzip(`in`.zip()) + assertThat<ExtendedEncoding>(out, `is`(`in`)) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/entity/SerializationTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/entity/SerializationTest.kt new file mode 100644 index 0000000..22b2d4f --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/entity/SerializationTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2017 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.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.* +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.TestBase +import ch.dissem.bitmessage.utils.TestUtils +import org.hamcrest.Matchers.`is` +import org.junit.Assert.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.util.* + +class SerializationTest : TestBase() { + @Test + fun `ensure GetPubkey is deserialized and serialized correctly`() { + doTest("V2GetPubkey.payload", 2, GetPubkey::class.java) + doTest("V3GetPubkey.payload", 2, GetPubkey::class.java) + doTest("V4GetPubkey.payload", 2, GetPubkey::class.java) + } + + @Test + fun `ensure V2Pubkey is deserialized and serialized correctly`() { + doTest("V2Pubkey.payload", 2, V2Pubkey::class.java) + } + + @Test + fun `ensure V3Pubkey is deserialized and serialized correctly`() { + doTest("V3Pubkey.payload", 3, V3Pubkey::class.java) + } + + @Test + fun `ensure V4Pubkey is deserialized and serialized correctly`() { + doTest("V4Pubkey.payload", 4, V4Pubkey::class.java) + } + + @Test + fun `ensure V1 msg is deserialized and serialized correctly`() { + doTest("V1Msg.payload", 1, Msg::class.java) + } + + @Test + fun `ensure V4Broadcast is deserialized and serialized correctly`() { + doTest("V4Broadcast.payload", 4, V4Broadcast::class.java) + } + + @Test + fun `ensure V5Broadcast is deserialized and serialized correctly`() { + doTest("V5Broadcast.payload", 5, V5Broadcast::class.java) + } + + @Test + fun `ensure unknown data is deserialized and serialized correctly`() { + doTest("V1MsgStrangeData.payload", 1, GenericPayload::class.java) + } + + @Test + fun `ensure plaintext is serialized and deserialized correctly`() { + val expected = Plaintext.Builder(MSG) + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .message("Subject", "Message") + .ackData("ackMessage".toByteArray()) + .signature(ByteArray(0)) + .build() + val out = ByteArrayOutputStream() + expected.write(out) + val `in` = ByteArrayInputStream(out.toByteArray()) + val actual = Plaintext.read(MSG, `in`) + + // Received is automatically set on deserialization, so we'll need to set it to null + val received = Plaintext::class.java.getDeclaredField("received") + received.isAccessible = true + received.set(actual, null) + + assertThat(expected, `is`(actual)) + } + + @Test + fun `ensure plaintext with extended encoding is serialized and deserialized correctly`() { + val expected = Plaintext.Builder(MSG) + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .message(Message.Builder() + .subject("Subject") + .body("Message") + .build()) + .ackData("ackMessage".toByteArray()) + .signature(ByteArray(0)) + .build() + val out = ByteArrayOutputStream() + expected.write(out) + val `in` = ByteArrayInputStream(out.toByteArray()) + val actual = Plaintext.read(MSG, `in`) + + // Received is automatically set on deserialization, so we'll need to set it to null + val received = Plaintext::class.java.getDeclaredField("received") + received.isAccessible = true + received.set(actual, null) + + assertEquals(expected, actual) + } + + @Test + fun `ensure plaintext with ack message is serialized and deserialized correctly`() { + val expected = Plaintext.Builder(MSG) + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .message("Subject", "Message") + .ackData("ackMessage".toByteArray()) + .signature(ByteArray(0)) + .build() + val ackMessage1 = expected.ackMessage + assertNotNull(ackMessage1) + + val out = ByteArrayOutputStream() + expected.write(out) + val `in` = ByteArrayInputStream(out.toByteArray()) + val actual = Plaintext.read(MSG, `in`) + + // Received is automatically set on deserialization, so we'll need to set it to null + val received = Plaintext::class.java.getDeclaredField("received") + received.isAccessible = true + received.set(actual, null) + + assertEquals(expected, actual) + assertEquals(ackMessage1, actual.ackMessage) + } + + @Test + fun `ensure network message is serialized and deserialized correctly`() { + val ivs = ArrayList<InventoryVector>(50000) + for (i in 0..49999) { + ivs.add(TestUtils.randomInventoryVector()) + } + + val inv = Inv(ivs) + val before = NetworkMessage(inv) + val out = ByteArrayOutputStream() + before.write(out) + + val after = Factory.getNetworkMessage(3, ByteArrayInputStream(out.toByteArray())) + assertNotNull(after) + val invAfter = after!!.payload as Inv + assertEquals(ivs, invAfter.inventory) + } + + private fun doTest(resourceName: String, version: Int, expectedPayloadType: Class<*>) { + val data = TestUtils.getBytes(resourceName) + val `in` = ByteArrayInputStream(data) + val objectMessage = Factory.getObjectMessage(version, `in`, data.size) + val out = ByteArrayOutputStream() + assertNotNull(objectMessage) + objectMessage!!.write(out) + assertArrayEquals(data, out.toByteArray()) + assertEquals(expectedPayloadType.canonicalName, objectMessage.payload.javaClass.canonicalName) + } + + @Test + fun `ensure system serialization works`() { + val plaintext = Plaintext.Builder(MSG) + .from(TestUtils.loadContact()) + .to(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .labels(listOf(Label("Test", Label.Type.INBOX, 0))) + .message("Test", "Test Test.\nTest") + .build() + val out = ByteArrayOutputStream() + val oos = ObjectOutputStream(out) + oos.writeObject(plaintext) + + val `in` = ByteArrayInputStream(out.toByteArray()) + val ois = ObjectInputStream(`in`) + assertEquals(plaintext, ois.readObject()) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.kt new file mode 100644 index 0000000..2bd3a0a --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2017 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.Singleton.cryptography +import ch.dissem.bitmessage.utils.TestBase +import org.junit.Assert.assertTrue +import org.junit.Test + +class ProofOfWorkEngineTest : TestBase() { + @Test(timeout = 90000) + fun `test SimplePOWEngine`() { + testPOW(SimplePOWEngine()) + } + + @Test(timeout = 90000) + fun `test MultiThreadedPOWEngine`() { + testPOW(MultiThreadedPOWEngine()) + } + + private fun testPOW(engine: ProofOfWorkEngine) { + val initialHash = cryptography().sha512(byteArrayOf(1, 3, 6, 4)) + val target = byteArrayOf(0, 0, 0, -1, -1, -1, -1, -1) + + val waiter1 = CallbackWaiter<ByteArray>() + engine.calculateNonce(initialHash, target, + object : ProofOfWorkEngine.Callback { + @Suppress("NAME_SHADOWING") + override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) { + waiter1.setValue(nonce) + } + }) + val nonce1 = waiter1.waitForValue()!! + println("Calculating nonce1 took ${waiter1.time}ms") + assertTrue(Bytes.lt(cryptography().doubleSha512(nonce1, initialHash), target, 8)) + + // Let's add a second (shorter) run to find possible multi threading issues + val initialHash2 = cryptography().sha512(byteArrayOf(1, 3, 6, 5)) + val target2 = byteArrayOf(0, 0, -1, -1, -1, -1, -1, -1) + + val waiter2 = CallbackWaiter<ByteArray>() + engine.calculateNonce(initialHash2, target2, + object : ProofOfWorkEngine.Callback { + @Suppress("NAME_SHADOWING") + override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) { + waiter2.setValue(nonce) + } + }) + val nonce2 = waiter2.waitForValue()!! + println("Calculating nonce1 took ${waiter2.time}ms") + assertTrue(Bytes.lt(cryptography().doubleSha512(nonce2, initialHash2), target2, 8)) + assertTrue("Second nonce1 must be quicker to find", waiter1.time > waiter2.time) + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/testutils/TestInventory.kt b/core/src/test/kotlin/ch/dissem/bitmessage/testutils/TestInventory.kt new file mode 100644 index 0000000..fcdae32 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/testutils/TestInventory.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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.testutils + +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.util.* + +class TestInventory : Inventory { + private val inventory = HashMap<InventoryVector, ObjectMessage>() + + override fun getInventory(vararg streams: Long): List<InventoryVector> { + return ArrayList(inventory.keys) + } + + override fun getMissing(offer: List<InventoryVector>, vararg streams: Long): List<InventoryVector> { + return offer + } + + override fun getObject(vector: InventoryVector): ObjectMessage? { + return inventory[vector] + } + + override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> { + return ArrayList(inventory.values) + } + + override fun storeObject(objectMessage: ObjectMessage) { + inventory.put(objectMessage.inventoryVector, objectMessage) + } + + override fun contains(objectMessage: ObjectMessage): Boolean { + return inventory.containsKey(objectMessage.inventoryVector) + } + + override fun cleanup() { + + } + + fun init(vararg resources: String) { + inventory.clear() + for (resource in resources) { + val version = Integer.parseInt(resource.substring(1, 2)) + val obj = TestUtils.loadObjectMessage(version, resource) + inventory.put(obj.inventoryVector, obj) + } + } +} diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/BytesTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/BytesTest.kt new file mode 100644 index 0000000..cabd0b9 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/BytesTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2017 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 org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Ignore +import org.junit.Test +import java.math.BigInteger +import java.util.* + +class BytesTest { + val rnd = Random() + + @Test + fun `ensure expands correctly`() { + val source = byteArrayOf(1) + val expected = byteArrayOf(0, 1) + assertArrayEquals(expected, Bytes.expand(source, 2)) + } + + @Test + fun `ensure increment carry works`() { + val bytes = byteArrayOf(0, -1) + Bytes.inc(bytes) + assertArrayEquals(TestUtils.int16(256), bytes) + } + + @Test + fun `test increment by value`() { + for (v in 0..255) { + for (i in 1..255) { + val bytes = byteArrayOf(0, v.toByte()) + Bytes.inc(bytes, i.toByte()) + assertArrayEquals("value = " + v + "; inc = " + i + "; expected = " + (v + i), TestUtils.int16(v + i), bytes) + } + } + } + + /** + * This test is used to compare different implementations of the single byte lt comparison. It an safely be ignored. + */ + @Test + @Ignore + fun `test lower than single byte`() { + val a = ByteArray(1) + val b = ByteArray(1) + for (i in 0..254) { + for (j in 0..254) { + println("a = $i\tb = $j") + a[0] = i.toByte() + b[0] = j.toByte() + assertEquals(i < j, Bytes.lt(a, b)) + } + } + } + + @Test + fun `test lower than`() { + for (i in 0..999) { + val a = BigInteger.valueOf(rnd.nextLong()).pow(rnd.nextInt(5) + 1).abs() + val b = BigInteger.valueOf(rnd.nextLong()).pow(rnd.nextInt(5) + 1).abs() + println("a = " + a.toString(16) + "\tb = " + b.toString(16)) + assertEquals(a.compareTo(b) == -1, Bytes.lt(a.toByteArray(), b.toByteArray())) + } + } + + @Test + fun `test lower than bounded`() { + for (i in 0..999) { + val a = BigInteger.valueOf(rnd.nextLong()).pow(rnd.nextInt(5) + 1).abs() + val b = BigInteger.valueOf(rnd.nextLong()).pow(rnd.nextInt(5) + 1).abs() + println("a = " + a.toString(16) + "\tb = " + b.toString(16)) + assertEquals(a.compareTo(b) == -1, Bytes.lt( + Bytes.expand(a.toByteArray(), 100), + Bytes.expand(b.toByteArray(), 100), + 100)) + } + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java b/core/src/test/kotlin/ch/dissem/bitmessage/utils/CollectionsTest.kt similarity index 61% rename from core/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java rename to core/src/test/kotlin/ch/dissem/bitmessage/utils/CollectionsTest.kt index ef54165..bfb3fe5 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/StringsTest.java +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/CollectionsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,19 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.utils -import org.junit.Test; +import org.junit.Test -import static org.junit.Assert.assertEquals; +import java.util.LinkedList -public class StringsTest { +import org.junit.Assert.assertEquals + +class CollectionsTest { @Test - public void testHexString() { - assertEquals("48656c6c6f21", Strings.hex("Hello!".getBytes()).toString()); - assertEquals("0001", Strings.hex(new byte[]{0, 1}).toString()); + fun `ensure select random returns maximum possible items`() { + val list = LinkedList<Int>() + list += 0..9 + assertEquals(9, Collections.selectRandom(9, list).size) } } diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt new file mode 100644 index 0000000..cb26e5f --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2017 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.BitmessageAddress +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.ports.MessageRepository +import ch.dissem.bitmessage.utils.TestUtils.RANDOM +import com.nhaarman.mockito_kotlin.* +import org.hamcrest.Matchers.`is` +import org.junit.Assert.assertThat +import org.junit.Test +import java.util.* + +class ConversationServiceTest : TestBase() { + private val alice = BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + private val bob = BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj") + + private val messageRepository = mock<MessageRepository>() + private val conversationService = spy(ConversationService(messageRepository)) + + @Test + fun `ensure conversation is sorted properly`() { + MockitoKotlin.registerInstanceCreator { UUID.randomUUID() } + val expected = conversation + + doReturn(expected).whenever(conversationService).getConversation(any<UUID>()) + val actual = conversationService.getConversation(UUID.randomUUID()) + assertThat(actual, `is`(expected)) + } + + private val conversation: List<Plaintext> + get() { + val result = LinkedList<Plaintext>() + + val older = plaintext(alice, bob, + Message.Builder() + .subject("hey there") + .body("does it work?") + .build(), + Plaintext.Status.SENT) + result.add(older) + + val root = plaintext(alice, bob, + Message.Builder() + .subject("new test") + .body("There's a new test in town!") + .build(), + Plaintext.Status.SENT) + result.add(root) + + result.add( + plaintext(bob, alice, + Message.Builder() + .subject("Re: new test (1a)") + .body("Nice!") + .addParent(root) + .build(), + Plaintext.Status.RECEIVED) + ) + + val latest = plaintext(bob, alice, + Message.Builder() + .subject("Re: new test (2b)") + .body("PS: it did work!") + .addParent(root) + .addParent(older) + .build(), + Plaintext.Status.RECEIVED) + result.add(latest) + + result.add( + plaintext(alice, bob, + Message.Builder() + .subject("Re: new test (2)") + .body("") + .addParent(latest) + .build(), + Plaintext.Status.DRAFT) + ) + + return result + } + + private var timer = 2 + + private fun plaintext(from: BitmessageAddress, to: BitmessageAddress, + content: ExtendedEncoding, status: Plaintext.Status): Plaintext { + val builder = Plaintext.Builder(MSG) + .IV(TestUtils.randomInventoryVector()) + .from(from) + .to(to) + .message(content) + .status(status) + if (status !== Plaintext.Status.DRAFT && status !== Plaintext.Status.DOING_PROOF_OF_WORK) { + builder.received(5L * ++timer - RANDOM.nextInt(10)) + } + return builder.build() + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java b/core/src/test/kotlin/ch/dissem/bitmessage/utils/DecodeTest.kt similarity index 54% rename from core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java rename to core/src/test/kotlin/ch/dissem/bitmessage/utils/DecodeTest.kt index 60d882f..f1cf244 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/DecodeTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,28 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.utils -import org.junit.Test; +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream -import java.io.*; - -import static org.junit.Assert.assertEquals; - -public class DecodeTest { +class DecodeTest { @Test - public void ensureDecodingWorks() throws Exception { + fun `ensure decoding works`() { // This should test all relevant cases for var_int and therefore also uint_16, uint_32 and int_64 - testCodec(0); - for (long i = 1; i > 0; i = 3 * i + 7) { - testCodec(i); + testCodec(0) + var i: Long = 1 + while (i > 0) { + testCodec(i) + i = 3 * i + 7 } } - private void testCodec(long number) throws IOException { - ByteArrayOutputStream is = new ByteArrayOutputStream(); - Encode.varInt(number, is); - assertEquals(number, Decode.varInt(new ByteArrayInputStream(is.toByteArray()))); + private fun testCodec(number: Long) { + val `is` = ByteArrayOutputStream() + Encode.varInt(number, `is`) + assertEquals(number, Decode.varInt(ByteArrayInputStream(`is`.toByteArray()))) } } diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/EncodeTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/EncodeTest.kt new file mode 100644 index 0000000..0fa4361 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/EncodeTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2017 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 org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayOutputStream + +class EncodeTest { + @Test + fun `test uint8`() { + var stream = ByteArrayOutputStream() + Encode.int8(0, stream) + checkBytes(stream, 0) + + stream = ByteArrayOutputStream() + Encode.int8(255, stream) + checkBytes(stream, 255) + } + + @Test + fun `test uint16`() { + var stream = ByteArrayOutputStream() + Encode.int16(0, stream) + checkBytes(stream, 0, 0) + + stream = ByteArrayOutputStream() + Encode.int16(513, stream) + checkBytes(stream, 2, 1) + } + + @Test + fun `test uint32`() { + var stream = ByteArrayOutputStream() + Encode.int32(0, stream) + checkBytes(stream, 0, 0, 0, 0) + + stream = ByteArrayOutputStream() + Encode.int32(67305985, stream) + checkBytes(stream, 4, 3, 2, 1) + + stream = ByteArrayOutputStream() + Encode.int32(3355443201L, stream) + checkBytes(stream, 200, 0, 0, 1) + } + + @Test + fun `test uint64`() { + var stream = ByteArrayOutputStream() + Encode.int64(0, stream) + checkBytes(stream, 0, 0, 0, 0, 0, 0, 0, 0) + + stream = ByteArrayOutputStream() + Encode.int64(578437695752307201L, stream) + checkBytes(stream, 8, 7, 6, 5, 4, 3, 2, 1) + + stream = ByteArrayOutputStream() + @Suppress("INTEGER_OVERFLOW") // 0xc800000000000001L + Encode.int64(200 * 72057594037927936L + 1, stream) + checkBytes(stream, 200, 0, 0, 0, 0, 0, 0, 1) + } + + @Test + fun `test varInt`() { + var stream = ByteArrayOutputStream() + Encode.varInt(0, stream) + checkBytes(stream, 0) + + stream = ByteArrayOutputStream() + Encode.varInt(252, stream) + checkBytes(stream, 252) + + stream = ByteArrayOutputStream() + Encode.varInt(253, stream) + checkBytes(stream, 253, 0, 253) + + stream = ByteArrayOutputStream() + Encode.varInt(65535, stream) + checkBytes(stream, 253, 255, 255) + + stream = ByteArrayOutputStream() + Encode.varInt(65536, stream) + checkBytes(stream, 254, 0, 1, 0, 0) + + stream = ByteArrayOutputStream() + Encode.varInt(4294967295L, stream) + checkBytes(stream, 254, 255, 255, 255, 255) + + stream = ByteArrayOutputStream() + Encode.varInt(4294967296L, stream) + checkBytes(stream, 255, 0, 0, 0, 1, 0, 0, 0, 0) + + stream = ByteArrayOutputStream() + Encode.varInt(-1L, stream) + checkBytes(stream, 255, 255, 255, 255, 255, 255, 255, 255, 255) + } + + + fun checkBytes(stream: ByteArrayOutputStream, vararg bytes: Int) { + assertEquals(bytes.size, stream.size()) + val streamBytes = stream.toByteArray() + + for (i in bytes.indices) { + assertEquals(bytes[i].toByte(), streamBytes[i]) + } + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/SqlStringsTest.java b/core/src/test/kotlin/ch/dissem/bitmessage/utils/SqlStringsTest.kt similarity index 65% rename from core/src/test/java/ch/dissem/bitmessage/utils/SqlStringsTest.java rename to core/src/test/kotlin/ch/dissem/bitmessage/utils/SqlStringsTest.kt index 2aa1113..1097e01 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/SqlStringsTest.java +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/SqlStringsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.utils -import org.junit.Test; +import org.junit.Test -import static org.junit.Assert.assertEquals; +import org.junit.Assert.assertEquals -public class SqlStringsTest { +class SqlStringsTest { @Test - public void ensureJoinWorksWithLongArray() { - long[] test = {1L, 2L}; - assertEquals("1, 2", SqlStrings.join(test).toString()); + fun `ensure join works with long array`() { + val test = longArrayOf(1L, 2L) + assertEquals("1, 2", SqlStrings.join(*test)) } } diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/StringsTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/StringsTest.kt new file mode 100644 index 0000000..815d8e8 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/StringsTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017 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 org.junit.Test + +import org.junit.Assert.assertEquals + +class StringsTest { + @Test + fun `test hex string`() { + assertEquals("48656c6c6f21", Strings.hex("Hello!".toByteArray())) + assertEquals("0001", Strings.hex(byteArrayOf(0, 1))) + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java b/core/src/test/kotlin/ch/dissem/bitmessage/utils/TestBase.kt similarity index 62% rename from core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java rename to core/src/test/kotlin/ch/dissem/bitmessage/utils/TestBase.kt index e688cb1..90e2b2d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/TestBase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,19 @@ * limitations under the License. */ -package ch.dissem.bitmessage.exception; +package ch.dissem.bitmessage.utils + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import org.junit.BeforeClass /** * @author Christian Basler */ -public class ApplicationException extends RuntimeException { - private static final long serialVersionUID = 1796776684126759324L; - - public ApplicationException(Throwable cause) { - super(cause); - } - - public ApplicationException(String message) { - super(message); +open class TestBase { + companion object { + @BeforeClass + @JvmStatic fun setUpClass() { + Singleton.initialize(BouncyCryptography()) + } } } diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/TestUtils.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/TestUtils.kt new file mode 100644 index 0000000..0d92726 --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/TestUtils.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2017 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.BitmessageContext +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.V4Pubkey +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +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 com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.spy +import org.junit.Assert.assertEquals +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.util.Random +import kotlin.NoSuchElementException + +/** + * If there's ever a need for this in production code, it should be rewritten to be more efficient. + */ +object TestUtils { + @JvmField val RANDOM = Random() + + @JvmStatic fun int16(number: Int): ByteArray { + val out = ByteArrayOutputStream() + Encode.int16(number, out) + return out.toByteArray() + } + + @JvmStatic fun loadObjectMessage(version: Int, resourceName: String): ObjectMessage { + val data = getBytes(resourceName) + val `in` = ByteArrayInputStream(data) + return Factory.getObjectMessage(version, `in`, data.size) ?: throw NoSuchElementException("error loading object message") + } + + @JvmStatic fun getBytes(resourceName: String): ByteArray { + val `in` = javaClass.classLoader.getResourceAsStream(resourceName) + val out = ByteArrayOutputStream() + val buffer = ByteArray(1024) + var len = `in`.read(buffer) + while (len != -1) { + out.write(buffer, 0, len) + len = `in`.read(buffer) + } + return out.toByteArray() + } + + @JvmStatic fun randomInventoryVector(): InventoryVector { + val bytes = ByteArray(32) + RANDOM.nextBytes(bytes) + return InventoryVector(bytes) + } + + @JvmStatic fun getResource(resourceName: String): InputStream { + return javaClass.classLoader.getResourceAsStream(resourceName) + } + + @JvmStatic fun loadIdentity(address: String): BitmessageAddress { + val privateKey = PrivateKey.read(TestUtils.getResource(address + ".privkey")) + val identity = BitmessageAddress(privateKey) + assertEquals(address, identity.address) + return identity + } + + @Throws(DecryptionFailedException::class) + @JvmStatic fun loadContact(): BitmessageAddress { + val address = BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + val objectMessage = TestUtils.loadObjectMessage(3, "V4Pubkey.payload") + objectMessage.decrypt(address.publicDecryptionKey) + address.pubkey = objectMessage.payload as V4Pubkey + return address + } + + @JvmStatic fun loadPubkey(address: BitmessageAddress) { + val bytes = getBytes(address.address + ".pubkey") + val pubkey = Factory.readPubkey(address.version, address.stream, ByteArrayInputStream(bytes), bytes.size, false) + address.pubkey = pubkey + } + + @JvmStatic fun mockedInternalContext( + cryptography: Cryptography = mock {}, + inventory: Inventory = mock {}, + nodeRegistry: NodeRegistry = mock {}, + networkHandler: NetworkHandler = mock {}, + addressRepository: AddressRepository = mock {}, + messageRepository: MessageRepository = mock {}, + proofOfWorkRepository: ProofOfWorkRepository = mock {}, + proofOfWorkEngine: ProofOfWorkEngine = mock {}, + customCommandHandler: CustomCommandHandler = mock {}, + listener: BitmessageContext.Listener = mock {}, + labeler: Labeler = mock {}, + port: Int = 0, + connectionTTL: Long = 0, + connectionLimit: Int = 0 + ): InternalContext { + return spy(InternalContext( + cryptography, + inventory, + nodeRegistry, + networkHandler, + addressRepository, + messageRepository, + proofOfWorkRepository, + proofOfWorkEngine, + customCommandHandler, + listener, + labeler, + "/Jabit:TEST/", + port, + connectionTTL, + connectionLimit + )) + } +} diff --git a/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/cryptography-bc/build.gradle b/cryptography-bc/build.gradle index 7dde574..8fb1175 100644 --- a/cryptography-bc/build.gradle +++ b/cryptography-bc/build.gradle @@ -12,7 +12,8 @@ uploadArchives { dependencies { compile project(':core') - compile 'org.bouncycastle:bcprov-jdk15on:1.56' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.21' + compile 'org.bouncycastle:bcprov-jdk15on' + testCompile 'junit:junit' + testCompile 'com.nhaarman:mockito-kotlin' + testCompile project(path: ':core', configuration: 'testArtifacts') } 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 deleted file mode 100644 index bc8ec93..0000000 --- a/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java +++ /dev/null @@ -1,145 +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.cryptography.bc; - -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.ports.AbstractCryptography; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.crypto.BufferedBlockCipher; -import org.bouncycastle.crypto.CipherParameters; -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.*; -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"); - - public BouncyCryptography() { - super(new BouncyCastleProvider()); - } - - @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(ALGORITHM_ECDSA, provider).generatePublic(keySpec); - - return doCheckSignature(data, signature, publicKey); - } catch (GeneralSecurityException e) { - throw new ApplicationException(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(ALGORITHM_ECDSA, provider) - .generatePrivate(keySpec); - - return doSign(data, privKey); - } catch (GeneralSecurityException e) { - throw new ApplicationException(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/cryptography-bc/src/main/kotlin/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.kt b/cryptography-bc/src/main/kotlin/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.kt new file mode 100644 index 0000000..5ce7027 --- /dev/null +++ b/cryptography-bc/src/main/kotlin/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.kt @@ -0,0 +1,125 @@ +/* + * 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.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.util.* + +/** + * 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. + */ +class BouncyCryptography : AbstractCryptography(BouncyCastleProvider()) { + + override fun crypt(encrypt: Boolean, data: ByteArray, key_e: ByteArray, initializationVector: ByteArray): ByteArray { + val cipher = PaddedBufferedBlockCipher( + CBCBlockCipher(AESEngine()), + PKCS7Padding() + ) + val params = ParametersWithIV(KeyParameter(key_e), initializationVector) + + cipher.init(encrypt, params) + + val buffer = ByteArray(cipher.getOutputSize(data.size)) + var length = cipher.processBytes(data, 0, data.size, buffer, 0) + try { + length += cipher.doFinal(buffer, length) + } catch (e: InvalidCipherTextException) { + throw IllegalArgumentException(e) + } + + if (length < buffer.size) { + return Arrays.copyOfRange(buffer, 0, length) + } + return buffer + } + + override fun createPublicKey(privateKey: ByteArray): ByteArray { + return EC_CURVE_PARAMETERS.g.multiply(keyToBigInt(privateKey)).normalize().getEncoded(false) + } + + private fun keyToPoint(publicKey: ByteArray): ECPoint { + val x = BigInteger(1, Arrays.copyOfRange(publicKey, 1, 33)) + val y = BigInteger(1, Arrays.copyOfRange(publicKey, 33, 65)) + return EC_CURVE_PARAMETERS.curve.createPoint(x, y) + } + + override fun isSignatureValid(data: ByteArray, signature: ByteArray, pubkey: Pubkey): Boolean { + val spec = ECParameterSpec( + EC_CURVE_PARAMETERS.curve, + EC_CURVE_PARAMETERS.g, + EC_CURVE_PARAMETERS.n, + EC_CURVE_PARAMETERS.h, + EC_CURVE_PARAMETERS.seed + ) + + val Q = keyToPoint(pubkey.signingKey) + val keySpec = ECPublicKeySpec(Q, spec) + val publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider).generatePublic(keySpec) + + return doCheckSignature(data, signature, publicKey) + } + + override fun getSignature(data: ByteArray, privateKey: PrivateKey): ByteArray { + val spec = ECParameterSpec( + EC_CURVE_PARAMETERS.curve, + EC_CURVE_PARAMETERS.g, + EC_CURVE_PARAMETERS.n, + EC_CURVE_PARAMETERS.h, + EC_CURVE_PARAMETERS.seed + ) + + val d = keyToBigInt(privateKey.privateSigningKey) + val keySpec = ECPrivateKeySpec(d, spec) + val privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider) + .generatePrivate(keySpec) + + return doSign(data, privKey) + } + + override fun multiply(k: ByteArray, r: ByteArray): ByteArray { + return keyToPoint(k).multiply(keyToBigInt(r)).normalize().getEncoded(false) + } + + override fun createPoint(x: ByteArray, y: ByteArray): ByteArray { + return EC_CURVE_PARAMETERS.curve.createPoint( + BigInteger(1, x), + BigInteger(1, y) + ).getEncoded(false) + } + + companion object { + private val EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1") + } +} diff --git a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java deleted file mode 100644 index b0937d3..0000000 --- a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package ch.dissem.bitmessage.security; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.GenericPayload; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; -import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; -import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.utils.CallbackWaiter; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.UnixTime; -import org.junit.BeforeClass; -import org.junit.Test; - -import javax.xml.bind.DatatypeConverter; -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import static ch.dissem.bitmessage.utils.UnixTime.DAY; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Christian Basler - */ -public class CryptographyTest { - public static final byte[] TEST_VALUE = "teststring".getBytes(); - public static final byte[] TEST_SHA1 = DatatypeConverter.parseHexBinary("" - + "b8473b86d4c2072ca9b08bd28e373e8253e865c4"); - public static final byte[] TEST_SHA512 = DatatypeConverter.parseHexBinary("" - + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" - + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72"); - public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" - + "cd566972b5e50104011a92b59fa8e0b1234851ae"); - - private static BouncyCryptography crypto; - - @BeforeClass - public static void setUp() { - crypto = new BouncyCryptography(); - Singleton.initialize(crypto); - InternalContext ctx = mock(InternalContext.class); - when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); - crypto.setContext(ctx); - } - - @Test - public void testRipemd160() { - assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE)); - } - - @Test - public void testSha1() { - assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE)); - } - - @Test - public void testSha512() { - assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE)); - } - - @Test - public void testChaining() { - assertArrayEquals(TEST_SHA512, crypto.sha512("test".getBytes(), "string".getBytes())); - } - - @Test - public void ensureDoubleHashYieldsSameResultAsHashOfHash() { - assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)); - } - - @Test(expected = IOException.class) - public void ensureExceptionForInsufficientProofOfWork() throws IOException { - ObjectMessage objectMessage = new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(UnixTime.now(+28 * DAY)) - .objectType(0) - .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0)) - .build(); - crypto.checkProofOfWork(objectMessage, 1000, 1000); - } - - @Test - public void testDoProofOfWork() throws Exception { - ObjectMessage objectMessage = new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * MINUTE)) - .objectType(0) - .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0)) - .build(); - final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); - crypto.doProofOfWork(objectMessage, 1000, 1000, - new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - waiter.setValue(nonce); - } - }); - objectMessage.setNonce(waiter.waitForValue()); - try { - crypto.checkProofOfWork(objectMessage, 1000, 1000); - } catch (InsufficientProofOfWorkException e) { - fail(e.getMessage()); - } - } - - @Test - public void ensureEncryptionAndDecryptionWorks() { - byte[] data = crypto.randomBytes(100); - byte[] key_e = crypto.randomBytes(32); - byte[] iv = crypto.randomBytes(16); - byte[] encrypted = crypto.crypt(true, data, key_e, iv); - byte[] decrypted = crypto.crypt(false, encrypted, key_e, iv); - assertArrayEquals(data, decrypted); - } - - @Test(expected = IllegalArgumentException.class) - public void ensureDecryptionFailsWithInvalidCypherText() { - byte[] data = crypto.randomBytes(128); - byte[] key_e = crypto.randomBytes(32); - byte[] iv = crypto.randomBytes(16); - crypto.crypt(false, data, key_e, iv); - } - - @Test - public void testMultiplication() { - byte[] a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); - byte[] A = crypto.createPublicKey(a); - - byte[] b = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); - byte[] B = crypto.createPublicKey(b); - - assertArrayEquals(crypto.multiply(A, b), crypto.multiply(B, a)); - } - - @Test - public void ensureSignatureIsValid() { - byte[] data = crypto.randomBytes(100); - PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); - byte[] signature = crypto.getSignature(data, privateKey); - assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(true)); - } - - @Test - public void ensureSignatureIsInvalidForTemperedData() { - byte[] data = crypto.randomBytes(100); - PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); - byte[] signature = crypto.getSignature(data, privateKey); - data[0]++; - assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(false)); - } -} diff --git a/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt b/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt new file mode 100644 index 0000000..1897512 --- /dev/null +++ b/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2017 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.security + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.GenericPayload +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException +import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine +import ch.dissem.bitmessage.ports.ProofOfWorkEngine +import ch.dissem.bitmessage.utils.CallbackWaiter +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TestUtils +import ch.dissem.bitmessage.utils.UnixTime +import ch.dissem.bitmessage.utils.UnixTime.DAY +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import ch.dissem.bitmessage.utils.UnixTime.now +import org.hamcrest.CoreMatchers.`is` +import org.junit.Assert.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.IOException +import javax.xml.bind.DatatypeConverter + +/** + * @author Christian Basler + */ +class CryptographyTest { + + @Test + fun testRipemd160() { + assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE)) + } + + @Test + fun testSha1() { + assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE)) + } + + @Test + fun testSha512() { + assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE)) + } + + @Test + fun testChaining() { + assertArrayEquals(TEST_SHA512, crypto.sha512("test".toByteArray(), "string".toByteArray())) + } + + @Test + fun ensureDoubleHashYieldsSameResultAsHashOfHash() { + assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)) + } + + @Test(expected = IOException::class) + fun ensureExceptionForInsufficientProofOfWork() { + val objectMessage = ObjectMessage.Builder() + .nonce(ByteArray(8)) + .expiresTime(UnixTime.now + 28 * DAY) + .objectType(0) + .payload(GenericPayload.read(0, 1, ByteArrayInputStream(ByteArray(0)), 0)) + .build() + crypto.checkProofOfWork(objectMessage, 1000, 1000) + } + + @Test + fun testDoProofOfWork() { + TestUtils.mockedInternalContext( + cryptography = crypto, + proofOfWorkEngine = MultiThreadedPOWEngine() + ) + val objectMessage = ObjectMessage( + nonce = ByteArray(8), + expiresTime = now + 2 * MINUTE, + type = 0, + payload = GenericPayload.read(0, 1, ByteArrayInputStream(ByteArray(0)), 0), + version = 0, + stream = 1 + ) + val waiter = CallbackWaiter<ByteArray>() + crypto.doProofOfWork(objectMessage, 1000, 1000, + object : ProofOfWorkEngine.Callback { + override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) { + waiter.setValue(nonce) + } + }) + objectMessage.nonce = waiter.waitForValue() + try { + crypto.checkProofOfWork(objectMessage, 1000, 1000) + } catch (e: InsufficientProofOfWorkException) { + fail(e.message) + } + } + + @Test + fun ensureEncryptionAndDecryptionWorks() { + val data = crypto.randomBytes(100) + val key_e = crypto.randomBytes(32) + val iv = crypto.randomBytes(16) + val encrypted = crypto.crypt(true, data, key_e, iv) + val decrypted = crypto.crypt(false, encrypted, key_e, iv) + assertArrayEquals(data, decrypted) + } + + @Test(expected = IllegalArgumentException::class) + fun ensureDecryptionFailsWithInvalidCypherText() { + val data = crypto.randomBytes(128) + val key_e = crypto.randomBytes(32) + val iv = crypto.randomBytes(16) + crypto.crypt(false, data, key_e, iv) + } + + @Test + fun testMultiplication() { + val a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE) + val A = crypto.createPublicKey(a) + + val b = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE) + val B = crypto.createPublicKey(b) + + assertArrayEquals(crypto.multiply(A, b), crypto.multiply(B, a)) + } + + @Test + fun ensureSignatureIsValid() { + val data = crypto.randomBytes(100) + val privateKey = PrivateKey(false, 1, 1000, 1000) + val signature = crypto.getSignature(data, privateKey) + assertThat(crypto.isSignatureValid(data, signature, privateKey.pubkey), `is`(true)) + } + + @Test + fun ensureSignatureIsInvalidForTemperedData() { + val data = crypto.randomBytes(100) + val privateKey = PrivateKey(false, 1, 1000, 1000) + val signature = crypto.getSignature(data, privateKey) + data[0]++ + assertThat(crypto.isSignatureValid(data, signature, privateKey.pubkey), `is`(false)) + } + + companion object { + val TEST_VALUE = "teststring".toByteArray() + val TEST_SHA1 = DatatypeConverter.parseHexBinary("" + + "b8473b86d4c2072ca9b08bd28e373e8253e865c4") + val TEST_SHA512 = DatatypeConverter.parseHexBinary("" + + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" + + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72") + val TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" + + "cd566972b5e50104011a92b59fa8e0b1234851ae") + + private val crypto = BouncyCryptography() + + init { + Singleton.initialize(crypto) + } + } +} diff --git a/cryptography-sc/build.gradle b/cryptography-sc/build.gradle index c18f68d..6e8085c 100644 --- a/cryptography-sc/build.gradle +++ b/cryptography-sc/build.gradle @@ -12,7 +12,8 @@ uploadArchives { dependencies { compile project(':core') - compile 'com.madgag.spongycastle:prov:1.54.0.0' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.21' + compile 'com.madgag.spongycastle:prov' + testCompile 'junit:junit' + testCompile 'com.nhaarman:mockito-kotlin' + testCompile project(path: ':core', configuration: 'testArtifacts') } 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 deleted file mode 100644 index f86e6c2..0000000 --- a/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java +++ /dev/null @@ -1,147 +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.cryptography.sc; - -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.ports.AbstractCryptography; -import org.spongycastle.asn1.x9.X9ECParameters; -import org.spongycastle.crypto.BufferedBlockCipher; -import org.spongycastle.crypto.CipherParameters; -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.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.PublicKey; -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"); - - public SpongyCryptography() { - super(new BouncyCastleProvider()); - } - - @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(ALGORITHM_ECDSA, provider).generatePublic(keySpec); - - return doCheckSignature(data, signature, publicKey); - } catch (GeneralSecurityException e) { - throw new ApplicationException(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(ALGORITHM_ECDSA, provider) - .generatePrivate(keySpec); - - return doSign(data, privKey); - } catch (GeneralSecurityException e) { - throw new ApplicationException(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/cryptography-sc/src/main/kotlin/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.kt b/cryptography-sc/src/main/kotlin/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.kt new file mode 100644 index 0000000..1db64b3 --- /dev/null +++ b/cryptography-sc/src/main/kotlin/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.kt @@ -0,0 +1,125 @@ +/* + * 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.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.util.* + +/** + * 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. + */ +open class SpongyCryptography : AbstractCryptography(BouncyCastleProvider()) { + + override fun crypt(encrypt: Boolean, data: ByteArray, key_e: ByteArray, initializationVector: ByteArray): ByteArray { + val cipher = PaddedBufferedBlockCipher( + CBCBlockCipher(AESEngine()), + PKCS7Padding() + ) + val params = ParametersWithIV(KeyParameter(key_e), initializationVector) + + cipher.init(encrypt, params) + + val buffer = ByteArray(cipher.getOutputSize(data.size)) + var length = cipher.processBytes(data, 0, data.size, buffer, 0) + try { + length += cipher.doFinal(buffer, length) + } catch (e: InvalidCipherTextException) { + throw IllegalArgumentException(e) + } + + if (length < buffer.size) { + return Arrays.copyOfRange(buffer, 0, length) + } + return buffer + } + + override fun createPublicKey(privateKey: ByteArray): ByteArray { + return EC_CURVE_PARAMETERS.g.multiply(keyToBigInt(privateKey)).normalize().getEncoded(false) + } + + private fun keyToPoint(publicKey: ByteArray): ECPoint { + val x = BigInteger(1, Arrays.copyOfRange(publicKey, 1, 33)) + val y = BigInteger(1, Arrays.copyOfRange(publicKey, 33, 65)) + return EC_CURVE_PARAMETERS.curve.createPoint(x, y) + } + + override fun isSignatureValid(data: ByteArray, signature: ByteArray, pubkey: Pubkey): Boolean { + val spec = ECParameterSpec( + EC_CURVE_PARAMETERS.curve, + EC_CURVE_PARAMETERS.g, + EC_CURVE_PARAMETERS.n, + EC_CURVE_PARAMETERS.h, + EC_CURVE_PARAMETERS.seed + ) + + val Q = keyToPoint(pubkey.signingKey) + val keySpec = ECPublicKeySpec(Q, spec) + val publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider).generatePublic(keySpec) + + return doCheckSignature(data, signature, publicKey) + } + + override fun getSignature(data: ByteArray, privateKey: PrivateKey): ByteArray { + val spec = ECParameterSpec( + EC_CURVE_PARAMETERS.curve, + EC_CURVE_PARAMETERS.g, + EC_CURVE_PARAMETERS.n, + EC_CURVE_PARAMETERS.h, + EC_CURVE_PARAMETERS.seed + ) + + val d = keyToBigInt(privateKey.privateSigningKey) + val keySpec = ECPrivateKeySpec(d, spec) + val privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider) + .generatePrivate(keySpec) + + return doSign(data, privKey) + } + + override fun multiply(k: ByteArray, r: ByteArray): ByteArray { + return keyToPoint(k).multiply(keyToBigInt(r)).normalize().getEncoded(false) + } + + override fun createPoint(x: ByteArray, y: ByteArray): ByteArray { + return EC_CURVE_PARAMETERS.curve.createPoint( + BigInteger(1, x), + BigInteger(1, y) + ).getEncoded(false) + } + + companion object { + private val EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1") + } +} diff --git a/cryptography-sc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java b/cryptography-sc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java deleted file mode 100644 index dc3cdea..0000000 --- a/cryptography-sc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package ch.dissem.bitmessage.security; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.GenericPayload; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; -import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; -import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.utils.CallbackWaiter; -import ch.dissem.bitmessage.utils.Singleton; -import ch.dissem.bitmessage.utils.UnixTime; -import org.junit.BeforeClass; -import org.junit.Test; - -import javax.xml.bind.DatatypeConverter; -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import static ch.dissem.bitmessage.utils.UnixTime.DAY; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Christian Basler - */ -public class CryptographyTest { - public static final byte[] TEST_VALUE = "teststring".getBytes(); - public static final byte[] TEST_SHA1 = DatatypeConverter.parseHexBinary("" - + "b8473b86d4c2072ca9b08bd28e373e8253e865c4"); - public static final byte[] TEST_SHA512 = DatatypeConverter.parseHexBinary("" - + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" - + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72"); - public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" - + "cd566972b5e50104011a92b59fa8e0b1234851ae"); - - private static SpongyCryptography crypto; - - @BeforeClass - public static void setUp() { - crypto = new SpongyCryptography(); - Singleton.initialize(crypto); - InternalContext ctx = mock(InternalContext.class); - when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); - crypto.setContext(ctx); - } - - @Test - public void testRipemd160() { - assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE)); - } - - @Test - public void testSha1() { - assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE)); - } - - @Test - public void testSha512() { - assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE)); - } - - @Test - public void testChaining() { - assertArrayEquals(TEST_SHA512, crypto.sha512("test".getBytes(), "string".getBytes())); - } - - @Test - public void ensureDoubleHashYieldsSameResultAsHashOfHash() { - assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)); - } - - @Test(expected = IOException.class) - public void ensureExceptionForInsufficientProofOfWork() throws IOException { - ObjectMessage objectMessage = new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(UnixTime.now(+28 * DAY)) - .objectType(0) - .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0)) - .build(); - crypto.checkProofOfWork(objectMessage, 1000, 1000); - } - - @Test - public void testDoProofOfWork() throws Exception { - ObjectMessage objectMessage = new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * MINUTE)) - .objectType(0) - .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0)) - .build(); - final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); - crypto.doProofOfWork(objectMessage, 1000, 1000, - new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - waiter.setValue(nonce); - } - }); - objectMessage.setNonce(waiter.waitForValue()); - try { - crypto.checkProofOfWork(objectMessage, 1000, 1000); - } catch (InsufficientProofOfWorkException e) { - fail(e.getMessage()); - } - } - - @Test - public void ensureEncryptionAndDecryptionWorks() { - byte[] data = crypto.randomBytes(100); - byte[] key_e = crypto.randomBytes(32); - byte[] iv = crypto.randomBytes(16); - byte[] encrypted = crypto.crypt(true, data, key_e, iv); - byte[] decrypted = crypto.crypt(false, encrypted, key_e, iv); - assertArrayEquals(data, decrypted); - } - - @Test(expected = IllegalArgumentException.class) - public void ensureDecryptionFailsWithInvalidCypherText() { - byte[] data = crypto.randomBytes(128); - byte[] key_e = crypto.randomBytes(32); - byte[] iv = crypto.randomBytes(16); - crypto.crypt(false, data, key_e, iv); - } - - @Test - public void testMultiplication() { - byte[] a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); - byte[] A = crypto.createPublicKey(a); - - byte[] b = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); - byte[] B = crypto.createPublicKey(b); - - assertArrayEquals(crypto.multiply(A, b), crypto.multiply(B, a)); - } - - @Test - public void ensureSignatureIsValid() { - byte[] data = crypto.randomBytes(100); - PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); - byte[] signature = crypto.getSignature(data, privateKey); - assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(true)); - } - - @Test - public void ensureSignatureIsInvalidForTemperedData() { - byte[] data = crypto.randomBytes(100); - PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); - byte[] signature = crypto.getSignature(data, privateKey); - data[0]++; - assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(false)); - } -} diff --git a/cryptography-sc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt b/cryptography-sc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt new file mode 100644 index 0000000..15a9313 --- /dev/null +++ b/cryptography-sc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2017 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.security + +import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.GenericPayload +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException +import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine +import ch.dissem.bitmessage.ports.ProofOfWorkEngine +import ch.dissem.bitmessage.utils.CallbackWaiter +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TestUtils +import ch.dissem.bitmessage.utils.UnixTime +import ch.dissem.bitmessage.utils.UnixTime.DAY +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import org.hamcrest.CoreMatchers.`is` +import org.junit.Assert.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.IOException +import javax.xml.bind.DatatypeConverter + +/** + * @author Christian Basler + */ +class CryptographyTest { + + @Test + fun testRipemd160() { + assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE)) + } + + @Test + fun testSha1() { + assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE)) + } + + @Test + fun testSha512() { + assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE)) + } + + @Test + fun testChaining() { + assertArrayEquals(TEST_SHA512, crypto.sha512("test".toByteArray(), "string".toByteArray())) + } + + @Test + fun ensureDoubleHashYieldsSameResultAsHashOfHash() { + assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)) + } + + @Test(expected = IOException::class) + fun ensureExceptionForInsufficientProofOfWork() { + val objectMessage = ObjectMessage.Builder() + .nonce(ByteArray(8)) + .expiresTime(UnixTime.now + 28 * DAY) + .objectType(0) + .payload(GenericPayload.read(0, 1, ByteArrayInputStream(ByteArray(0)), 0)) + .build() + crypto.checkProofOfWork(objectMessage, 1000, 1000) + } + + @Test + fun testDoProofOfWork() { + TestUtils.mockedInternalContext( + cryptography = crypto, + proofOfWorkEngine = MultiThreadedPOWEngine() + ) + val objectMessage = ObjectMessage( + nonce = ByteArray(8), + expiresTime = UnixTime.now + 2 * MINUTE, + type = 0, + payload = GenericPayload.read(0, 1, ByteArrayInputStream(ByteArray(0)), 0), + version = 0, + stream = 1 + ) + val waiter = CallbackWaiter<ByteArray>() + crypto.doProofOfWork(objectMessage, 1000, 1000, + object : ProofOfWorkEngine.Callback { + override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) { + waiter.setValue(nonce) + } + }) + objectMessage.nonce = waiter.waitForValue() + try { + crypto.checkProofOfWork(objectMessage, 1000, 1000) + } catch (e: InsufficientProofOfWorkException) { + fail(e.message) + } + } + + @Test + fun ensureEncryptionAndDecryptionWorks() { + val data = crypto.randomBytes(100) + val key_e = crypto.randomBytes(32) + val iv = crypto.randomBytes(16) + val encrypted = crypto.crypt(true, data, key_e, iv) + val decrypted = crypto.crypt(false, encrypted, key_e, iv) + assertArrayEquals(data, decrypted) + } + + @Test(expected = IllegalArgumentException::class) + fun ensureDecryptionFailsWithInvalidCypherText() { + val data = crypto.randomBytes(128) + val key_e = crypto.randomBytes(32) + val iv = crypto.randomBytes(16) + crypto.crypt(false, data, key_e, iv) + } + + @Test + fun testMultiplication() { + val a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE) + val A = crypto.createPublicKey(a) + + val b = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE) + val B = crypto.createPublicKey(b) + + assertArrayEquals(crypto.multiply(A, b), crypto.multiply(B, a)) + } + + @Test + fun ensureSignatureIsValid() { + val data = crypto.randomBytes(100) + val privateKey = PrivateKey(false, 1, 1000, 1000) + val signature = crypto.getSignature(data, privateKey) + assertThat(crypto.isSignatureValid(data, signature, privateKey.pubkey), `is`(true)) + } + + @Test + fun ensureSignatureIsInvalidForTemperedData() { + val data = crypto.randomBytes(100) + val privateKey = PrivateKey(false, 1, 1000, 1000) + val signature = crypto.getSignature(data, privateKey) + data[0]++ + assertThat(crypto.isSignatureValid(data, signature, privateKey.pubkey), `is`(false)) + } + + companion object { + val TEST_VALUE = "teststring".toByteArray() + val TEST_SHA1 = DatatypeConverter.parseHexBinary("" + + "b8473b86d4c2072ca9b08bd28e373e8253e865c4") + val TEST_SHA512 = DatatypeConverter.parseHexBinary("" + + "6253b39071e5df8b5098f59202d414c37a17d6a38a875ef5f8c7d89b0212b028" + + "692d3d2090ce03ae1de66c862fa8a561e57ed9eb7935ce627344f742c0931d72") + val TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" + + "cd566972b5e50104011a92b59fa8e0b1234851ae") + + private val crypto = SpongyCryptography() + + init { + Singleton.initialize(crypto) + } + } +} diff --git a/demo/build.gradle b/demo/build.gradle index 159dbc0..3699414 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -15,6 +15,7 @@ uploadArchives { } sourceCompatibility = 1.8 +targetCompatibility = 1.8 test.enabled = Boolean.valueOf(systemTestsEnabled) @@ -28,10 +29,10 @@ dependencies { compile project(':repositories') compile project(':cryptography-bc') compile project(':wif') - compile 'org.slf4j:slf4j-simple:1.7.25' - compile 'args4j:args4j:2.33' - compile 'com.h2database:h2:1.4.194' - compile 'org.apache.commons:commons-lang3:3.5' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.21' + compile 'org.slf4j:slf4j-simple' + compile 'args4j:args4j' + compile 'com.h2database:h2' + compile 'org.apache.commons:commons-lang3' + testCompile 'junit:junit' + testCompile 'com.nhaarman:mockito-kotlin' } 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 1da2910..40ee063 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java @@ -46,6 +46,8 @@ public class Main { if (System.getProperty("org.slf4j.simpleLogger.logFile") == null) System.setProperty("org.slf4j.simpleLogger.logFile", "./jabit.log"); + System.out.println("Version: " + BitmessageContext.getVersion()); + CmdLineOptions options = new CmdLineOptions(); CmdLineParser parser = new CmdLineParser(options); try { @@ -81,8 +83,8 @@ public class Main { } @Override - public void offerAddresses(List<NetworkAddress> addresses) { - LOG.info("Local node registry ignored offered addresses: " + addresses); + public void offerAddresses(List<NetworkAddress> nodes) { + LOG.info("Local node registry ignored offered addresses: " + nodes); } }); } else { diff --git a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java index 9f97432..2cfe84f 100644 --- a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java +++ b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java @@ -1,31 +1,43 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.bitmessage; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.DefaultLabeler; import ch.dissem.bitmessage.ports.Labeler; -import ch.dissem.bitmessage.ports.NetworkHandler; import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.utils.TTL; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; -import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static com.nhaarman.mockito_kotlin.MockitoKt.spy; +import static com.nhaarman.mockito_kotlin.MockitoKt.timeout; +import static com.nhaarman.mockito_kotlin.MockitoKt.verify; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -33,74 +45,62 @@ import static org.mockito.ArgumentMatchers.any; /** * @author Christian Basler */ -@RunWith(Parameterized.class) public class SystemTest { private static int port = 6000; - private final NetworkHandler aliceNetworkHandler; - private final NetworkHandler bobNetworkHandler; private BitmessageContext alice; - private TestListener aliceListener = new TestListener(); - private Labeler aliceLabeler = Mockito.spy(new DebugLabeler("Alice")); private BitmessageAddress aliceIdentity; + private Labeler aliceLabeler; private BitmessageContext bob; - private TestListener bobListener = new TestListener(); + private TestListener bobListener; private BitmessageAddress bobIdentity; - public SystemTest(NetworkHandler peer, NetworkHandler node) { - this.aliceNetworkHandler = peer; - this.bobNetworkHandler = node; - } - - @Parameterized.Parameters - @SuppressWarnings("deprecation") - public static List<Object[]> parameters() { - return Arrays.asList(new Object[][]{ - {new NioNetworkHandler(), new DefaultNetworkHandler()}, - {new NioNetworkHandler(), new NioNetworkHandler()} - }); - } - @Before public void setUp() { - int alicePort = port++; - int bobPort = port++; TTL.msg(5 * MINUTE); TTL.getpubkey(5 * MINUTE); TTL.pubkey(5 * MINUTE); - JdbcConfig aliceDB = new JdbcConfig("jdbc:h2:mem:alice;DB_CLOSE_DELAY=-1", "sa", ""); - alice = new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(aliceDB)) - .inventory(new JdbcInventory(aliceDB)) - .messageRepo(new JdbcMessageRepository(aliceDB)) - .powRepo(new JdbcProofOfWorkRepository(aliceDB)) - .port(alicePort) - .nodeRegistry(new TestNodeRegistry(bobPort)) - .networkHandler(aliceNetworkHandler) - .cryptography(new BouncyCryptography()) - .listener(aliceListener) - .labeler(aliceLabeler) - .build(); - alice.startup(); - aliceIdentity = alice.createIdentity(false, DOES_ACK); - - JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); - bob = new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(bobDB)) - .inventory(new JdbcInventory(bobDB)) - .messageRepo(new JdbcMessageRepository(bobDB)) - .powRepo(new JdbcProofOfWorkRepository(bobDB)) - .port(bobPort) - .nodeRegistry(new TestNodeRegistry(alicePort)) - .networkHandler(bobNetworkHandler) - .cryptography(new BouncyCryptography()) - .listener(bobListener) - .labeler(new DebugLabeler("Bob")) - .build(); - bob.startup(); - bobIdentity = bob.createIdentity(false, DOES_ACK); + int alicePort = port++; + int bobPort = port++; + { + JdbcConfig aliceDB = new JdbcConfig("jdbc:h2:mem:alice;DB_CLOSE_DELAY=-1", "sa", ""); + aliceLabeler = spy(new DebugLabeler("Alice")); + TestListener aliceListener = new TestListener(); + alice = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(aliceDB)) + .inventory(new JdbcInventory(aliceDB)) + .messageRepo(new JdbcMessageRepository(aliceDB)) + .powRepo(new JdbcProofOfWorkRepository(aliceDB)) + .port(alicePort) + .nodeRegistry(new TestNodeRegistry(bobPort)) + .networkHandler(new NioNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(aliceListener) + .labeler(aliceLabeler) + .build(); + alice.startup(); + aliceIdentity = alice.createIdentity(false, DOES_ACK); + } + { + JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); + bobListener = new TestListener(); + bob = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(bobDB)) + .inventory(new JdbcInventory(bobDB)) + .messageRepo(new JdbcMessageRepository(bobDB)) + .powRepo(new JdbcProofOfWorkRepository(bobDB)) + .port(bobPort) + .nodeRegistry(new TestNodeRegistry(alicePort)) + .networkHandler(new NioNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(bobListener) + .labeler(new DebugLabeler("Bob")) + .build(); + bob.startup(); + bobIdentity = bob.createIdentity(false, DOES_ACK); + } ((DebugLabeler) alice.labeler()).init(aliceIdentity, bobIdentity); ((DebugLabeler) bob.labeler()).init(aliceIdentity, bobIdentity); } @@ -111,17 +111,17 @@ public class SystemTest { bob.shutdown(); } - @Test(timeout = 60_000) + @Test(timeout = 120_000) public void ensureAliceCanSendMessageToBob() throws Exception { String originalMessage = UUID.randomUUID().toString(); alice.send(aliceIdentity, new BitmessageAddress(bobIdentity.getAddress()), "Subject", originalMessage); - Plaintext plaintext = bobListener.get(15, TimeUnit.MINUTES); + Plaintext plaintext = bobListener.get(2, TimeUnit.MINUTES); assertThat(plaintext.getType(), equalTo(Plaintext.Type.MSG)); assertThat(plaintext.getText(), equalTo(originalMessage)); - Mockito.verify(aliceLabeler, Mockito.timeout(TimeUnit.MINUTES.toMillis(15)).atLeastOnce()) + verify(aliceLabeler, timeout(TimeUnit.MINUTES.toMillis(2)).atLeastOnce()) .markAsAcknowledged(any()); } diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestListener.java b/demo/src/test/java/ch/dissem/bitmessage/TestListener.java deleted file mode 100644 index 9c00776..0000000 --- a/demo/src/test/java/ch/dissem/bitmessage/TestListener.java +++ /dev/null @@ -1,26 +0,0 @@ -package ch.dissem.bitmessage; - -import ch.dissem.bitmessage.entity.Plaintext; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -/** - * Created by chrig on 02.02.2016. - */ -public class TestListener implements BitmessageContext.Listener { - private CompletableFuture<Plaintext> future = new CompletableFuture<>(); - - @Override - public void receive(Plaintext plaintext) { - future.complete(plaintext); - } - - public void reset() { - future = new CompletableFuture<>(); - } - - public Plaintext get(long timeout, TimeUnit unit) throws Exception { - return future.get(timeout, unit); - } -} diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestListener.kt b/demo/src/test/java/ch/dissem/bitmessage/TestListener.kt new file mode 100644 index 0000000..abb1bfd --- /dev/null +++ b/demo/src/test/java/ch/dissem/bitmessage/TestListener.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage + +import ch.dissem.bitmessage.entity.Plaintext + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +/** + * Test listener that allows you to wait for a message in tests. + */ +class TestListener : BitmessageContext.Listener { + private var future = CompletableFuture<Plaintext>() + + override fun receive(plaintext: Plaintext) { + future.complete(plaintext) + } + + fun reset() { + future = CompletableFuture<Plaintext>() + } + + operator fun get(timeout: Long, unit: TimeUnit): Plaintext { + return future.get(timeout, unit) + } +} diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java b/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java index 2f9f070..09f7a85 100644 --- a/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java +++ b/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java @@ -31,10 +31,10 @@ class TestNodeRegistry implements NodeRegistry { public TestNodeRegistry(int... ports) { for (int port : ports) { nodes.add( - new NetworkAddress.Builder() - .ipv4(127, 0, 0, 1) - .port(port) - .build() + new NetworkAddress.Builder() + .ipv4(127, 0, 0, 1) + .port(port) + .build() ); } } @@ -50,7 +50,7 @@ class TestNodeRegistry implements NodeRegistry { } @Override - public void offerAddresses(List<NetworkAddress> addresses) { + public void offerAddresses(List<NetworkAddress> nodes) { // Ignore } } diff --git a/extensions/build.gradle b/extensions/build.gradle index 1b3cc8b..2ec242e 100644 --- a/extensions/build.gradle +++ b/extensions/build.gradle @@ -28,9 +28,9 @@ uploadArchives { dependencies { compile project(':core') - testCompile 'junit:junit:4.12' - testCompile 'org.slf4j:slf4j-simple:1.7.25' - testCompile 'org.mockito:mockito-core:2.7.21' + testCompile 'junit:junit' + testCompile 'org.slf4j:slf4j-simple' + testCompile 'com.nhaarman:mockito-kotlin' 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 deleted file mode 100644 index 7955e5b..0000000 --- a/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java +++ /dev/null @@ -1,146 +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.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.cryptography; - -/** - * A {@link CustomMessage} implementation that contains signed and encrypted data. - * - * @author Christian Basler - */ -public class CryptoCustomMessage<T extends Streamable> extends CustomMessage { - private static final long serialVersionUID = 7395193565986284426L; - - public static final String COMMAND = "ENCRYPTED"; - - private final Reader<T> dataReader; - private CryptoBox container; - private BitmessageAddress sender; - private T data; - - public CryptoCustomMessage(T data) throws IOException { - super(COMMAND); - this.data = data; - this.dataReader = null; - } - - private CryptoCustomMessage(CryptoBox container, Reader<T> dataReader) { - super(COMMAND); - this.container = container; - this.dataReader = dataReader; - } - - public static <T extends Streamable> CryptoCustomMessage<T> read(CustomMessage data, Reader<T> dataReader) throws IOException { - CryptoBox cryptoBox = CryptoBox.read(new ByteArrayInputStream(data.getData()), data.getData().length); - return new CryptoCustomMessage<>(cryptoBox, dataReader); - } - - public BitmessageAddress getSender() { - return sender; - } - - public void signAndEncrypt(BitmessageAddress identity, byte[] publicKey) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - Encode.varInt(identity.getVersion(), out); - Encode.varInt(identity.getStream(), out); - Encode.int32(identity.getPubkey().getBehaviorBitfield(), out); - out.write(identity.getPubkey().getSigningKey(), 1, 64); - out.write(identity.getPubkey().getEncryptionKey(), 1, 64); - if (identity.getVersion() >= 3) { - Encode.varInt(identity.getPubkey().getNonceTrialsPerByte(), out); - Encode.varInt(identity.getPubkey().getExtraBytes(), out); - } - - data.write(out); - Encode.varBytes(cryptography().getSignature(out.toByteArray(), identity.getPrivateKey()), out); - container = new CryptoBox(out.toByteArray(), publicKey); - } - - public T decrypt(byte[] privateKey) throws IOException, DecryptionFailedException { - SignatureCheckingInputStream in = new SignatureCheckingInputStream(container.decrypt(privateKey)); - - long addressVersion = varInt(in); - long stream = varInt(in); - int behaviorBitfield = int32(in); - byte[] publicSigningKey = bytes(in, 64); - byte[] publicEncryptionKey = bytes(in, 64); - long nonceTrialsPerByte = addressVersion >= 3 ? varInt(in) : 0; - long extraBytes = addressVersion >= 3 ? varInt(in) : 0; - - sender = new BitmessageAddress(Factory.createPubkey( - addressVersion, - stream, - publicSigningKey, - publicEncryptionKey, - nonceTrialsPerByte, - extraBytes, - behaviorBitfield - )); - - data = dataReader.read(sender, in); - - in.checkSignature(sender.getPubkey()); - - return data; - } - - @Override - public void write(OutputStream out) throws IOException { - Encode.varString(COMMAND, out); - container.write(out); - } - - public interface Reader<T> { - T read(BitmessageAddress sender, InputStream in) throws IOException; - } - - private class SignatureCheckingInputStream extends InputStream { - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); - private final InputStream wrapped; - - private SignatureCheckingInputStream(InputStream wrapped) { - this.wrapped = wrapped; - } - - @Override - public int read() throws IOException { - int read = wrapped.read(); - if (read >= 0) out.write(read); - return read; - } - - public void checkSignature(Pubkey pubkey) throws IOException, IllegalStateException { - if (!cryptography().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { - throw new IllegalStateException("Signature check failed"); - } - } - } -} diff --git a/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java b/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java deleted file mode 100644 index e5ba4f8..0000000 --- a/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java +++ /dev/null @@ -1,134 +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.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.nio.ByteBuffer; -import java.util.Arrays; - -import static ch.dissem.bitmessage.utils.Decode.*; - -/** - * @author Christian Basler - */ -public class ProofOfWorkRequest implements Streamable { - private static final long serialVersionUID = 4729003751499662713L; - - private final BitmessageAddress sender; - private final byte[] initialHash; - private final Request request; - - private final byte[] data; - - public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request) { - this(sender, initialHash, request, new byte[0]); - } - - public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request, byte[] data) { - this.sender = sender; - this.initialHash = initialHash; - this.request = request; - this.data = data; - } - - public static ProofOfWorkRequest read(BitmessageAddress client, InputStream in) throws IOException { - return new ProofOfWorkRequest( - client, - bytes(in, 64), - Request.valueOf(varString(in)), - varBytes(in) - ); - } - - public BitmessageAddress getSender() { - return sender; - } - - public byte[] getInitialHash() { - return initialHash; - } - - public Request getRequest() { - return request; - } - - public byte[] getData() { - return data; - } - - @Override - public void write(OutputStream out) throws IOException { - out.write(initialHash); - Encode.varString(request.name(), out); - Encode.varBytes(data, out); - } - - @Override - public void write(ByteBuffer buffer) { - buffer.put(initialHash); - Encode.varString(request.name(), buffer); - Encode.varBytes(data, buffer); - } - - @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/main/kotlin/ch/dissem/bitmessage/extensions/CryptoCustomMessage.kt b/extensions/src/main/kotlin/ch/dissem/bitmessage/extensions/CryptoCustomMessage.kt new file mode 100644 index 0000000..d5a2349 --- /dev/null +++ b/extensions/src/main/kotlin/ch/dissem/bitmessage/extensions/CryptoCustomMessage.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2017 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.Decode.bytes +import ch.dissem.bitmessage.utils.Decode.int32 +import ch.dissem.bitmessage.utils.Decode.varBytes +import ch.dissem.bitmessage.utils.Decode.varInt +import ch.dissem.bitmessage.utils.Encode +import ch.dissem.bitmessage.utils.Singleton.cryptography +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream + +/** + * A [CustomMessage] implementation that contains signed and encrypted data. + + * @author Christian Basler + */ +class CryptoCustomMessage<T : Streamable> : CustomMessage { + + private val dataReader: Reader<T>? + private var container: CryptoBox? = null + var sender: BitmessageAddress? = null + private set + private var data: T? = null + private set + + constructor(data: T) : super(COMMAND, null) { + this.data = data + this.dataReader = null + } + + private constructor(container: CryptoBox, dataReader: Reader<T>) : super(COMMAND, null) { + this.container = container + this.dataReader = dataReader + } + + fun signAndEncrypt(identity: BitmessageAddress, publicKey: ByteArray) { + val out = ByteArrayOutputStream() + + val privateKey = identity.privateKey ?: throw IllegalStateException("signing identity must have a private key") + + Encode.varInt(identity.version, out) + Encode.varInt(identity.stream, out) + Encode.int32(privateKey.pubkey.behaviorBitfield, out) + out.write(privateKey.pubkey.signingKey, 1, 64) + out.write(privateKey.pubkey.encryptionKey, 1, 64) + if (identity.version >= 3) { + Encode.varInt(privateKey.pubkey.nonceTrialsPerByte, out) + Encode.varInt(privateKey.pubkey.extraBytes, out) + } + + data?.write(out) ?: throw IllegalStateException("no unencrypted data available") + Encode.varBytes(cryptography().getSignature(out.toByteArray(), privateKey), out) + container = CryptoBox(out.toByteArray(), publicKey) + } + + @Throws(DecryptionFailedException::class) + fun decrypt(privateKey: ByteArray): T { + val `in` = SignatureCheckingInputStream(container?.decrypt(privateKey) ?: throw IllegalStateException("no encrypted data available")) + if (dataReader == null) throw IllegalStateException("no data reader available") + + val addressVersion = varInt(`in`) + val stream = varInt(`in`) + val behaviorBitfield = int32(`in`) + val publicSigningKey = bytes(`in`, 64) + val publicEncryptionKey = bytes(`in`, 64) + val nonceTrialsPerByte = if (addressVersion >= 3) varInt(`in`) else 0 + val extraBytes = if (addressVersion >= 3) varInt(`in`) else 0 + + val sender = BitmessageAddress(Factory.createPubkey( + addressVersion, + stream, + publicSigningKey, + publicEncryptionKey, + nonceTrialsPerByte, + extraBytes, + behaviorBitfield + )) + this.sender = sender + + data = dataReader.read(sender, `in`) + + `in`.checkSignature(sender.pubkey!!) + + return data!! + } + + override fun write(out: OutputStream) { + Encode.varString(COMMAND, out) + container?.write(out) ?: throw IllegalStateException("not encrypted yet") + } + + interface Reader<out T> { + fun read(sender: BitmessageAddress, `in`: InputStream): T + } + + private inner class SignatureCheckingInputStream internal constructor(private val wrapped: InputStream) : InputStream() { + private val out = ByteArrayOutputStream() + + override fun read(): Int { + val read = wrapped.read() + if (read >= 0) out.write(read) + return read + } + + @Throws(IllegalStateException::class) + fun checkSignature(pubkey: Pubkey) { + if (!cryptography().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { + throw IllegalStateException("Signature check failed") + } + } + } + + companion object { + @JvmField val COMMAND = "ENCRYPTED" + + @JvmStatic fun <T : Streamable> read(data: CustomMessage, dataReader: Reader<T>): CryptoCustomMessage<T> { + val cryptoBox = CryptoBox.read(ByteArrayInputStream(data.getData()), data.getData().size) + return CryptoCustomMessage(cryptoBox, dataReader) + } + } +} diff --git a/extensions/src/main/kotlin/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.kt b/extensions/src/main/kotlin/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.kt new file mode 100644 index 0000000..ef987c3 --- /dev/null +++ b/extensions/src/main/kotlin/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2017 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.Decode.bytes +import ch.dissem.bitmessage.utils.Decode.varBytes +import ch.dissem.bitmessage.utils.Decode.varString +import ch.dissem.bitmessage.utils.Encode +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.util.* + +/** + * @author Christian Basler + */ +data class ProofOfWorkRequest @JvmOverloads constructor(val sender: BitmessageAddress, val initialHash: ByteArray, val request: ProofOfWorkRequest.Request, val data: ByteArray = ByteArray(0)) : Streamable { + + override fun write(out: OutputStream) { + out.write(initialHash) + Encode.varString(request.name, out) + Encode.varBytes(data, out) + } + + override fun write(buffer: ByteBuffer) { + buffer.put(initialHash) + Encode.varString(request.name, buffer) + Encode.varBytes(data, buffer) + } + + class Reader(private val identity: BitmessageAddress) : CryptoCustomMessage.Reader<ProofOfWorkRequest> { + + override fun read(sender: BitmessageAddress, `in`: InputStream): ProofOfWorkRequest { + return ProofOfWorkRequest.read(identity, `in`) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ProofOfWorkRequest) return false + + if (sender != 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 fun hashCode(): Int { + var result = sender.hashCode() + result = 31 * result + Arrays.hashCode(initialHash) + result = 31 * result + request.hashCode() + result = 31 * result + Arrays.hashCode(data) + return result + } + + enum class Request { + CALCULATE, + CALCULATING, + COMPLETE + } + + companion object { + fun read(client: BitmessageAddress, `in`: InputStream): ProofOfWorkRequest { + return ProofOfWorkRequest( + client, + bytes(`in`, 64), + Request.valueOf(varString(`in`)), + varBytes(`in`) + ) + } + } +} diff --git a/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java b/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java deleted file mode 100644 index bf0fa6d..0000000 --- a/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java +++ /dev/null @@ -1,86 +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.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.cryptography; -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, cryptography().randomBytes(100)); - CryptoCustomMessage<GenericPayload> messageBefore = new CryptoCustomMessage<>(payloadBefore); - messageBefore.signAndEncrypt(sendingIdentity, cryptography().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, 1, in, 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, cryptography().randomBytes(64), - ProofOfWorkRequest.Request.CALCULATE); - - CryptoCustomMessage<ProofOfWorkRequest> messageBefore = new CryptoCustomMessage<>(requestBefore); - messageBefore.signAndEncrypt(sendingIdentity, cryptography().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/extensions/src/test/kotlin/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.kt b/extensions/src/test/kotlin/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.kt new file mode 100644 index 0000000..f722fa0 --- /dev/null +++ b/extensions/src/test/kotlin/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2017 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.InputStream + +import ch.dissem.bitmessage.utils.Singleton.cryptography +import org.junit.Assert.assertEquals + +class CryptoCustomMessageTest : TestBase() { + @Test + fun `ensure encrypt then decrypt yields same object`() { + val privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")) + val sendingIdentity = BitmessageAddress(privateKey) + + val payloadBefore = GenericPayload(0, 1, cryptography().randomBytes(100)) + val messageBefore = CryptoCustomMessage(payloadBefore) + messageBefore.signAndEncrypt(sendingIdentity, cryptography().createPublicKey(sendingIdentity.publicDecryptionKey)) + + val out = ByteArrayOutputStream() + messageBefore.write(out) + val `in` = ByteArrayInputStream(out.toByteArray()) + + val customMessage = CustomMessage.read(`in`, out.size()) + val messageAfter = CryptoCustomMessage.read(customMessage, + object : CryptoCustomMessage.Reader<GenericPayload> { + override fun read(sender: BitmessageAddress, `in`: InputStream): GenericPayload { + return GenericPayload.read(0, 1, `in`, 100) + } + }) + val payloadAfter = messageAfter.decrypt(sendingIdentity.publicDecryptionKey) + + assertEquals(payloadBefore, payloadAfter) + } + + @Test + fun `test with actual request`() { + val privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")) + val sendingIdentity = BitmessageAddress(privateKey) + + val requestBefore = ProofOfWorkRequest(sendingIdentity, cryptography().randomBytes(64), + ProofOfWorkRequest.Request.CALCULATE) + + val messageBefore = CryptoCustomMessage(requestBefore) + messageBefore.signAndEncrypt(sendingIdentity, cryptography().createPublicKey(sendingIdentity.publicDecryptionKey)) + + + val out = ByteArrayOutputStream() + messageBefore.write(out) + val `in` = ByteArrayInputStream(out.toByteArray()) + + val customMessage = CustomMessage.read(`in`, out.size()) + val messageAfter = CryptoCustomMessage.read(customMessage, + ProofOfWorkRequest.Reader(sendingIdentity)) + val requestAfter = messageAfter.decrypt(sendingIdentity.publicDecryptionKey) + + assertEquals(requestBefore, requestAfter) + } +} diff --git a/gradle.properties b/gradle.properties index bb8beb4..ad2d684 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,8 @@ # 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= - -systemTestsEnabled=false \ No newline at end of file +systemTestsEnabled=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 3baa851..c9f10ca 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f15c32..0cf2d0f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Apr 03 17:55:37 CEST 2017 +#Sun Jul 02 11:22:52 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip diff --git a/gradlew b/gradlew index 27309d9..4453cce 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f6d5974..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -49,7 +49,6 @@ goto fail @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/networking/build.gradle b/networking/build.gradle index b01eb40..489729c 100644 --- a/networking/build.gradle +++ b/networking/build.gradle @@ -12,9 +12,9 @@ uploadArchives { dependencies { compile project(':core') - testCompile 'junit:junit:4.12' - testCompile 'org.slf4j:slf4j-simple:1.7.25' - testCompile 'org.mockito:mockito-core:2.7.21' + testCompile 'junit:junit' + testCompile 'org.slf4j:slf4j-simple' + testCompile 'com.nhaarman:mockito-kotlin' testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') } diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/AbstractConnection.java b/networking/src/main/java/ch/dissem/bitmessage/networking/AbstractConnection.java deleted file mode 100644 index 4ad759a..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/AbstractConnection.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking; - -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.*; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; -import ch.dissem.bitmessage.exception.NodeException; -import ch.dissem.bitmessage.ports.NetworkHandler; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; - -import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; -import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SERVER; -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SYNC; -import static ch.dissem.bitmessage.networking.AbstractConnection.State.*; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; - -/** - * Contains everything used by both the old streams-oriented NetworkHandler and the new NioNetworkHandler, - * respectively their connection objects. - */ -public abstract class AbstractConnection { - private static final Logger LOG = LoggerFactory.getLogger(AbstractConnection.class); - protected final InternalContext ctx; - protected final Mode mode; - protected final NetworkAddress host; - protected final NetworkAddress node; - protected final NetworkHandler.MessageListener listener; - protected final Map<InventoryVector, Long> ivCache; - protected final Deque<MessagePayload> sendingQueue; - protected final Map<InventoryVector, Long> commonRequestedObjects; - protected final Set<InventoryVector> requestedObjects; - - protected volatile State state; - protected long lastObjectTime; - - private final long syncTimeout; - private long syncReadTimeout = Long.MAX_VALUE; - - protected long peerNonce; - protected int version; - protected long[] streams; - private boolean verackSent; - private boolean verackReceived; - - public AbstractConnection(InternalContext context, Mode mode, - NetworkAddress node, - Map<InventoryVector, Long> commonRequestedObjects, - long syncTimeout) { - this.ctx = context; - this.mode = mode; - this.host = new NetworkAddress.Builder().ipv6(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).port(0).build(); - this.node = node; - this.listener = context.getNetworkListener(); - this.syncTimeout = (syncTimeout > 0 ? UnixTime.now(+syncTimeout) : 0); - this.requestedObjects = Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)); - this.ivCache = new ConcurrentHashMap<>(); - this.sendingQueue = new ConcurrentLinkedDeque<>(); - this.state = CONNECTING; - this.commonRequestedObjects = commonRequestedObjects; - } - - public Mode getMode() { - return mode; - } - - public NetworkAddress getNode() { - return node; - } - - public State getState() { - return state; - } - - public long[] getStreams() { - return streams; - } - - protected void handleMessage(MessagePayload payload) { - switch (state) { - case ACTIVE: - receiveMessage(payload); - break; - - case DISCONNECTED: - break; - - default: - handleCommand(payload); - break; - } - } - - private void receiveMessage(MessagePayload messagePayload) { - switch (messagePayload.getCommand()) { - case INV: - receiveMessage((Inv) messagePayload); - break; - case GETDATA: - receiveMessage((GetData) messagePayload); - break; - case OBJECT: - receiveMessage((ObjectMessage) messagePayload); - break; - case ADDR: - receiveMessage((Addr) messagePayload); - break; - case CUSTOM: - case VERACK: - case VERSION: - default: - throw new IllegalStateException("Unexpectedly received '" + messagePayload.getCommand() + "' command"); - } - } - - private void receiveMessage(Inv inv) { - int originalSize = inv.getInventory().size(); - updateIvCache(inv.getInventory()); - List<InventoryVector> missing = ctx.getInventory().getMissing(inv.getInventory(), streams); - missing.removeAll(commonRequestedObjects.keySet()); - LOG.trace("Received inventory with " + originalSize + " elements, of which are " - + missing.size() + " missing."); - send(new GetData.Builder().inventory(missing).build()); - } - - private void receiveMessage(GetData getData) { - for (InventoryVector iv : getData.getInventory()) { - ObjectMessage om = ctx.getInventory().getObject(iv); - if (om != null) sendingQueue.offer(om); - } - } - - private void receiveMessage(ObjectMessage objectMessage) { - requestedObjects.remove(objectMessage.getInventoryVector()); - if (ctx.getInventory().contains(objectMessage)) { - LOG.trace("Received object " + objectMessage.getInventoryVector() + " - already in inventory"); - return; - } - try { - listener.receive(objectMessage); - cryptography().checkProofOfWork(objectMessage, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES); - ctx.getInventory().storeObject(objectMessage); - // offer object to some random nodes so it gets distributed throughout the network: - ctx.getNetworkHandler().offer(objectMessage.getInventoryVector()); - lastObjectTime = UnixTime.now(); - } catch (InsufficientProofOfWorkException e) { - LOG.warn(e.getMessage()); - // DebugUtils.saveToFile(objectMessage); // this line must not be committed active - } catch (IOException e) { - LOG.error("Stream " + objectMessage.getStream() + ", object type " + objectMessage.getType() + ": " + e.getMessage(), e); - } finally { - if (commonRequestedObjects.remove(objectMessage.getInventoryVector()) == null) { - LOG.debug("Received object that wasn't requested."); - } - } - } - - private void receiveMessage(Addr addr) { - LOG.trace("Received " + addr.getAddresses().size() + " addresses."); - ctx.getNodeRegistry().offerAddresses(addr.getAddresses()); - } - - private void updateIvCache(List<InventoryVector> inventory) { - cleanupIvCache(); - Long now = UnixTime.now(); - for (InventoryVector iv : inventory) { - ivCache.put(iv, now); - } - } - - public void offer(InventoryVector iv) { - sendingQueue.offer(new Inv.Builder() - .addInventoryVector(iv) - .build()); - updateIvCache(Collections.singletonList(iv)); - } - - public boolean knowsOf(InventoryVector iv) { - return ivCache.containsKey(iv); - } - - public boolean requested(InventoryVector iv) { - return requestedObjects.contains(iv); - } - - private void cleanupIvCache() { - Long fiveMinutesAgo = UnixTime.now(-5 * MINUTE); - for (Map.Entry<InventoryVector, Long> entry : ivCache.entrySet()) { - if (entry.getValue() < fiveMinutesAgo) { - ivCache.remove(entry.getKey()); - } - } - } - - private void handleCommand(MessagePayload payload) { - switch (payload.getCommand()) { - case VERSION: - handleVersion((Version) payload); - break; - case VERACK: - if (verackSent) { - activateConnection(); - } - verackReceived = true; - break; - case CUSTOM: - MessagePayload response = ctx.getCustomCommandHandler().handle((CustomMessage) payload); - if (response == null) { - disconnect(); - } else { - send(response); - } - break; - default: - throw new NodeException("Command 'version' or 'verack' expected, but was '" - + payload.getCommand() + "'"); - } - } - - private void activateConnection() { - LOG.info("Successfully established connection with node " + node); - state = ACTIVE; - node.setTime(UnixTime.now()); - if (mode != SYNC) { - sendAddresses(); - ctx.getNodeRegistry().offerAddresses(Collections.singletonList(node)); - } - sendInventory(); - } - - private void sendAddresses() { - List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses(1000, streams); - sendingQueue.offer(new Addr.Builder().addresses(addresses).build()); - } - - private void sendInventory() { - List<InventoryVector> inventory = ctx.getInventory().getInventory(streams); - for (int i = 0; i < inventory.size(); i += 50000) { - sendingQueue.offer(new Inv.Builder() - .inventory(inventory.subList(i, Math.min(inventory.size(), i + 50000))) - .build()); - } - } - - private void handleVersion(Version version) { - if (version.getNonce() == ctx.getClientNonce()) { - LOG.info("Tried to connect to self, disconnecting."); - disconnect(); - } else if (version.getVersion() >= BitmessageContext.CURRENT_VERSION) { - this.peerNonce = version.getNonce(); - if (peerNonce == ctx.getClientNonce()) disconnect(); - - this.version = version.getVersion(); - this.streams = version.getStreams(); - verackSent = true; - send(new VerAck()); - if (mode == SERVER) { - send(new Version.Builder().defaults(ctx.getClientNonce()).addrFrom(host).addrRecv(node).build()); - } - if (verackReceived) { - activateConnection(); - } - } else { - LOG.info("Received unsupported version " + version.getVersion() + ", disconnecting."); - disconnect(); - } - } - - @SuppressWarnings("RedundantIfStatement") - protected 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 (!sendingQueue.isEmpty()) { - syncReadTimeout = System.currentTimeMillis() + 1000; - return false; - } - if (msg == null) { - return syncReadTimeout < System.currentTimeMillis(); - } else { - syncReadTimeout = System.currentTimeMillis() + 1000; - return false; - } - } - - public void disconnect() { - state = DISCONNECTED; - - // Make sure objects that are still missing are requested from other nodes - ctx.getNetworkHandler().request(requestedObjects); - } - - protected abstract void send(MessagePayload payload); - - public enum Mode {SERVER, CLIENT, SYNC} - - public enum State {CONNECTING, ACTIVE, DISCONNECTED} - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AbstractConnection that = (AbstractConnection) o; - return Objects.equals(node, that.node); - } - - @Override - public int hashCode() { - return Objects.hash(node); - } -} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java b/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java deleted file mode 100644 index 4dcf29c..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java +++ /dev/null @@ -1,225 +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.entity.GetData; -import ch.dissem.bitmessage.entity.MessagePayload; -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.entity.Version; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.NetworkHandler.MessageListener; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -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; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.CLIENT; -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SYNC; -import static ch.dissem.bitmessage.networking.AbstractConnection.State.ACTIVE; -import static ch.dissem.bitmessage.networking.AbstractConnection.State.DISCONNECTED; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; - -/** - * A connection to a specific node - */ -class Connection extends AbstractConnection { - public static final int READ_TIMEOUT = 2000; - private static final Logger LOG = LoggerFactory.getLogger(Connection.class); - private static final int CONNECT_TIMEOUT = 5000; - - private final long startTime; - private final Socket socket; - private final ReaderRunnable reader = new ReaderRunnable(); - private final WriterRunnable writer = new WriterRunnable(); - - private InputStream in; - private OutputStream out; - private boolean socketInitialized; - - public Connection(InternalContext context, Mode mode, Socket socket, - Map<InventoryVector, Long> requestedObjectsMap) throws IOException { - this(context, mode, socket, requestedObjectsMap, - new NetworkAddress.Builder().ip(socket.getInetAddress()).port(socket.getPort()).stream(1).build(), - 0); - } - - public Connection(InternalContext context, Mode mode, NetworkAddress node, - Map<InventoryVector, Long> requestedObjectsMap) { - this(context, mode, new Socket(), requestedObjectsMap, - node, 0); - } - - private Connection(InternalContext context, Mode mode, Socket socket, - Map<InventoryVector, Long> commonRequestedObjects, NetworkAddress node, long syncTimeout) { - super(context, mode, node, commonRequestedObjects, syncTimeout); - this.startTime = UnixTime.now(); - this.socket = socket; - } - - public static Connection sync(InternalContext ctx, InetAddress address, int port, MessageListener listener, - long timeoutInSeconds) throws IOException { - return new Connection(ctx, SYNC, new Socket(address, port), - new HashMap<InventoryVector, Long>(), - new NetworkAddress.Builder().ip(address).port(port).stream(1).build(), - timeoutInSeconds); - } - - public long getStartTime() { - return startTime; - } - - public Mode getMode() { - return mode; - } - - public State getState() { - return state; - } - - public NetworkAddress getNode() { - return node; - } - - @Override - protected void send(MessagePayload payload) { - try { - 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(); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Connection that = (Connection) o; - return Objects.equals(node, that.node); - } - - @Override - public int hashCode() { - return Objects.hash(node); - } - - 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 ReaderRunnable getReader() { - return reader; - } - - public WriterRunnable getWriter() { - return writer; - } - - public class ReaderRunnable implements Runnable { - @Override - public void run() { - try (Socket socket = Connection.this.socket) { - initSocket(socket); - if (mode == CLIENT || mode == SYNC) { - send(new Version.Builder().defaults(ctx.getClientNonce()).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); - } - } - receive(); - } - } catch (Exception e) { - LOG.trace("Reader disconnected from node " + node + ": " + e.getMessage()); - } finally { - disconnect(); - try { - socket.close(); - } catch (Exception e) { - LOG.debug(e.getMessage(), e); - } - } - } - - private void receive() throws InterruptedException { - try { - NetworkMessage msg = Factory.getNetworkMessage(version, in); - if (msg == null) - return; - handleMessage(msg.getPayload()); - if (socket.isClosed() || syncFinished(msg) || checkOpenRequests()) disconnect(); - } catch (SocketTimeoutException ignore) { - if (state == ACTIVE && syncFinished(null)) disconnect(); - } - } - - } - - 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()) { - Thread.sleep(1000); - } else { - send(sendingQueue.poll()); - } - } - } 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/ConnectionOrganizer.java b/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java deleted file mode 100644 index 6d7ae04..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Iterator; -import java.util.List; - -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.CLIENT; -import static ch.dissem.bitmessage.networking.DefaultNetworkHandler.NETWORK_MAGIC_NUMBER; - -/** - * @author Christian Basler - */ -@Deprecated -@SuppressWarnings("deprecation") -public class ConnectionOrganizer implements Runnable { - private static final Logger LOG = LoggerFactory.getLogger(ConnectionOrganizer.class); - - private final InternalContext ctx; - private final DefaultNetworkHandler networkHandler; - - private Connection initialConnection; - - public ConnectionOrganizer(InternalContext ctx, - DefaultNetworkHandler networkHandler) { - this.ctx = ctx; - this.networkHandler = networkHandler; - } - - @Override - public void run() { - try { - while (networkHandler.isRunning()) { - try { - int active = 0; - long now = UnixTime.now(); - - int diff = networkHandler.connections.size() - ctx.getConnectionLimit(); - if (diff > 0) { - for (Connection c : networkHandler.connections) { - c.disconnect(); - diff--; - if (diff == 0) break; - } - } - boolean forcedDisconnect = false; - for (Iterator<Connection> iterator = networkHandler.connections.iterator(); iterator.hasNext(); ) { - Connection c = iterator.next(); - // Just in case they were all created at the same time, don't disconnect - // all at once. - if (!forcedDisconnect && now - c.getStartTime() > ctx.getConnectionTTL()) { - c.disconnect(); - forcedDisconnect = true; - } - switch (c.getState()) { - case DISCONNECTED: - iterator.remove(); - break; - case ACTIVE: - active++; - break; - default: - // nothing to do - } - } - - if (active < NETWORK_MAGIC_NUMBER) { - List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses( - NETWORK_MAGIC_NUMBER - active, ctx.getStreams()); - boolean first = active == 0 && initialConnection == null; - for (NetworkAddress address : addresses) { - Connection c = new Connection(ctx, CLIENT, address, networkHandler.requestedObjects); - if (first) { - initialConnection = c; - first = false; - } - networkHandler.startConnection(c); - } - Thread.sleep(10000); - } else if (initialConnection == null) { - Thread.sleep(30000); - } else { - initialConnection.disconnect(); - initialConnection = null; - Thread.sleep(10000); - } - } catch (InterruptedException e) { - networkHandler.stop(); - } catch (Exception e) { - LOG.error("Error in connection manager. Ignored.", e); - } - } - } finally { - LOG.debug("Connection manager shutting down."); - networkHandler.stop(); - } - } -} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java deleted file mode 100644 index b0966d4..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java +++ /dev/null @@ -1,248 +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.CustomMessage; -import ch.dissem.bitmessage.entity.GetData; -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.exception.NodeException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.NetworkHandler; -import ch.dissem.bitmessage.utils.Collections; -import ch.dissem.bitmessage.utils.Property; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.util.*; -import java.util.concurrent.*; - -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SERVER; -import static ch.dissem.bitmessage.networking.AbstractConnection.State.ACTIVE; -import static ch.dissem.bitmessage.utils.DebugUtils.inc; -import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool; - -/** - * Handles all the networky stuff. - * - * @deprecated use {@link ch.dissem.bitmessage.networking.nio.NioNetworkHandler NioNetworkHandler} instead. - */ -@Deprecated -public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { - - final Collection<Connection> connections = new ConcurrentLinkedQueue<>(); - private final ExecutorService pool = Executors.newCachedThreadPool( - pool("network") - .lowPrio() - .daemon() - .build()); - private InternalContext ctx; - private ServerRunnable server; - private volatile boolean running; - - final Map<InventoryVector, Long> requestedObjects = new ConcurrentHashMap<>(50_000); - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } - - @Override - public Future<?> synchronize(InetAddress server, int port, long timeoutInSeconds) { - try { - Connection connection = Connection.sync(ctx, server, port, ctx.getNetworkListener(), timeoutInSeconds); - Future<?> reader = pool.submit(connection.getReader()); - pool.execute(connection.getWriter()); - return reader; - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - @Override - public CustomMessage send(InetAddress server, int port, CustomMessage request) { - try (Socket socket = new Socket(server, port)) { - socket.setSoTimeout(Connection.READ_TIMEOUT); - new NetworkMessage(request).write(socket.getOutputStream()); - NetworkMessage networkMessage = Factory.getNetworkMessage(3, socket.getInputStream()); - if (networkMessage != null && networkMessage.getPayload() instanceof CustomMessage) { - return (CustomMessage) networkMessage.getPayload(); - } else { - if (networkMessage == null) { - throw new NodeException("No response from node " + server); - } else { - throw new NodeException("Unexpected response from node " + - server + ": " + networkMessage.getPayload().getCommand()); - } - } - } catch (IOException e) { - throw new NodeException(e.getMessage(), e); - } - } - - @Override - public void start() { - if (running) { - throw new IllegalStateException("Network already running - you need to stop first."); - } - try { - running = true; - connections.clear(); - server = new ServerRunnable(ctx, this); - pool.execute(server); - pool.execute(new ConnectionOrganizer(ctx, this)); - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - @Override - public boolean isRunning() { - return running; - } - - @Override - public void stop() { - server.close(); - synchronized (connections) { - running = false; - for (Connection c : connections) { - c.disconnect(); - } - } - requestedObjects.clear(); - } - - void startConnection(Connection c) { - if (!running) return; - - synchronized (connections) { - if (!running) return; - - // 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<>(); - 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<>(); - - for (Connection connection : connections) { - if (connection.getState() == ACTIVE) { - for (long stream : connection.getStreams()) { - 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()) - ); - } - - @Override - public void request(Collection<InventoryVector> inventoryVectors) { - if (!running || inventoryVectors.isEmpty()) return; - - Map<Connection, List<InventoryVector>> distribution = new HashMap<>(); - for (Connection connection : connections) { - if (connection.getState() == ACTIVE) { - distribution.put(connection, new LinkedList<InventoryVector>()); - } - } - Iterator<InventoryVector> iterator = inventoryVectors.iterator(); - if (!iterator.hasNext()) { - return; - } - InventoryVector next = iterator.next(); - Connection previous = null; - do { - for (Connection connection : distribution.keySet()) { - if (connection == previous) { - next = iterator.next(); - } - if (connection.knowsOf(next)) { - List<InventoryVector> ivs = distribution.get(connection); - if (ivs.size() == GetData.MAX_INVENTORY_SIZE) { - connection.send(new GetData.Builder().inventory(ivs).build()); - ivs.clear(); - } - ivs.add(next); - iterator.remove(); - - if (iterator.hasNext()) { - next = iterator.next(); - previous = connection; - } else { - break; - } - } - } - } while (iterator.hasNext()); - - for (Connection connection : distribution.keySet()) { - List<InventoryVector> ivs = distribution.get(connection); - if (!ivs.isEmpty()) { - connection.send(new GetData.Builder().inventory(ivs).build()); - } - } - } -} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java b/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java deleted file mode 100644 index 0a4e7a2..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.ports.NetworkHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Closeable; -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; - -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SERVER; - -/** - * @author Christian Basler - */ -@Deprecated -public class ServerRunnable implements Runnable, Closeable { - private static final Logger LOG = LoggerFactory.getLogger(ServerRunnable.class); - private final InternalContext ctx; - private final ServerSocket serverSocket; - private final DefaultNetworkHandler networkHandler; - private final NetworkHandler.MessageListener listener; - - public ServerRunnable(InternalContext ctx, DefaultNetworkHandler networkHandler) throws IOException { - this.ctx = ctx; - this.networkHandler = networkHandler; - this.listener = ctx.getNetworkListener(); - this.serverSocket = new ServerSocket(ctx.getPort()); - } - - @Override - public void run() { - while (!serverSocket.isClosed()) { - try { - Socket socket = serverSocket.accept(); - socket.setSoTimeout(Connection.READ_TIMEOUT); - networkHandler.startConnection(new Connection(ctx, SERVER, socket, networkHandler.requestedObjects)); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - } - } - - @Override - public void close() { - try { - serverSocket.close(); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - } -} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/ConnectionInfo.java b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/ConnectionInfo.java deleted file mode 100644 index 012b43f..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/ConnectionInfo.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking.nio; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.GetData; -import ch.dissem.bitmessage.entity.MessagePayload; -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.entity.Version; -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.V3MessageReader; -import ch.dissem.bitmessage.networking.AbstractConnection; -import ch.dissem.bitmessage.utils.UnixTime; - -import java.nio.ByteBuffer; -import java.util.*; - -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.CLIENT; -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SYNC; - -/** - * Represents the current state of a connection. - */ -public class ConnectionInfo extends AbstractConnection { - private final ByteBuffer headerOut = ByteBuffer.allocate(24); - private ByteBuffer payloadOut; - private V3MessageReader reader = new V3MessageReader(); - private boolean syncFinished; - private long lastUpdate = System.currentTimeMillis(); - - public ConnectionInfo(InternalContext context, Mode mode, NetworkAddress node, - Map<InventoryVector, Long> commonRequestedObjects, long syncTimeout) { - super(context, mode, node, commonRequestedObjects, syncTimeout); - headerOut.flip(); - if (mode == CLIENT || mode == SYNC) { - send(new Version.Builder().defaults(ctx.getClientNonce()).addrFrom(host).addrRecv(node).build()); - } - } - - public State getState() { - return state; - } - - public boolean knowsOf(InventoryVector iv) { - return ivCache.containsKey(iv); - } - - public Queue<MessagePayload> getSendingQueue() { - return sendingQueue; - } - - public ByteBuffer getInBuffer() { - if (reader == null) { - throw new NodeException("Node is disconnected"); - } - return reader.getActiveBuffer(); - } - - public void updateWriter() { - if (!headerOut.hasRemaining() && !sendingQueue.isEmpty()) { - headerOut.clear(); - MessagePayload payload = sendingQueue.poll(); - payloadOut = new NetworkMessage(payload).writeHeaderAndGetPayloadBuffer(headerOut); - headerOut.flip(); - lastUpdate = System.currentTimeMillis(); - } - } - - public ByteBuffer[] getOutBuffers() { - return new ByteBuffer[]{headerOut, payloadOut}; - } - - public void cleanupBuffers() { - if (payloadOut != null && !payloadOut.hasRemaining()) { - payloadOut = null; - } - } - - public void updateReader() { - reader.update(); - if (!reader.getMessages().isEmpty()) { - Iterator<NetworkMessage> iterator = reader.getMessages().iterator(); - NetworkMessage msg = null; - while (iterator.hasNext()) { - msg = iterator.next(); - handleMessage(msg.getPayload()); - iterator.remove(); - } - syncFinished = syncFinished(msg); - } - lastUpdate = System.currentTimeMillis(); - } - - public void updateSyncStatus() { - if (!syncFinished) { - syncFinished = (reader == null || reader.getMessages().isEmpty()) && syncFinished(null); - } - } - - public boolean isExpired() { - switch (state) { - case CONNECTING: - // the TCP timeout starts out at 20 seconds - return lastUpdate < System.currentTimeMillis() - 20_000; - case ACTIVE: - // after verack messages are exchanged, the timeout is raised to 10 minutes - return lastUpdate < System.currentTimeMillis() - 600_000; - case DISCONNECTED: - return true; - default: - throw new IllegalStateException("Unknown state: " + state); - } - } - - @Override - public void disconnect() { - super.disconnect(); - if (reader != null) { - reader.cleanup(); - reader = null; - } - payloadOut = null; - } - - public boolean isSyncFinished() { - return syncFinished; - } - - @Override - protected void send(MessagePayload payload) { - sendingQueue.add(payload); - if (payload instanceof GetData) { - Long now = UnixTime.now(); - List<InventoryVector> inventory = ((GetData) payload).getInventory(); - requestedObjects.addAll(inventory); - for (InventoryVector iv : inventory) { - commonRequestedObjects.put(iv, now); - } - } - } - - public boolean isWritePending() { - return !sendingQueue.isEmpty() - || headerOut != null && headerOut.hasRemaining() - || payloadOut != null && payloadOut.hasRemaining(); - } -} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java deleted file mode 100644 index 04e6d4d..0000000 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java +++ /dev/null @@ -1,521 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.networking.nio; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.CustomMessage; -import ch.dissem.bitmessage.entity.GetData; -import ch.dissem.bitmessage.entity.NetworkMessage; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.exception.NodeException; -import ch.dissem.bitmessage.factory.V3MessageReader; -import ch.dissem.bitmessage.ports.NetworkHandler; -import ch.dissem.bitmessage.utils.Property; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.NoRouteToHostException; -import java.nio.ByteBuffer; -import java.nio.channels.*; -import java.util.*; -import java.util.concurrent.*; - -import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.*; -import static ch.dissem.bitmessage.networking.AbstractConnection.State.ACTIVE; -import static ch.dissem.bitmessage.networking.AbstractConnection.State.DISCONNECTED; -import static ch.dissem.bitmessage.utils.Collections.selectRandom; -import static ch.dissem.bitmessage.utils.DebugUtils.inc; -import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool; -import static java.nio.channels.SelectionKey.*; - -/** - * Network handler using java.nio, resulting in less threads. - */ -public class NioNetworkHandler implements NetworkHandler, InternalContext.ContextHolder { - private static final Logger LOG = LoggerFactory.getLogger(NioNetworkHandler.class); - private static final long REQUESTED_OBJECTS_MAX_TIME = 2 * 60_000; // 2 minutes - private static final Long DELAYED = Long.MIN_VALUE; - - private final ExecutorService threadPool = Executors.newCachedThreadPool( - pool("network") - .lowPrio() - .daemon() - .build()); - - private InternalContext ctx; - private Selector selector; - private ServerSocketChannel serverChannel; - private Queue<NetworkAddress> connectionQueue = new ConcurrentLinkedQueue<>(); - private Map<ConnectionInfo, SelectionKey> connections = new ConcurrentHashMap<>(); - private final Map<InventoryVector, Long> requestedObjects = new ConcurrentHashMap<>(10_000); - - private Thread starter; - - @Override - public Future<Void> synchronize(final InetAddress server, final int port, final long timeoutInSeconds) { - return threadPool.submit(new Callable<Void>() { - @Override - public Void call() throws Exception { - try (SocketChannel channel = SocketChannel.open(new InetSocketAddress(server, port))) { - channel.configureBlocking(false); - ConnectionInfo connection = new ConnectionInfo(ctx, SYNC, - new NetworkAddress.Builder().ip(server).port(port).stream(1).build(), - new HashMap<InventoryVector, Long>(), timeoutInSeconds); - while (channel.isConnected() && !connection.isSyncFinished()) { - write(channel, connection); - read(channel, connection); - Thread.sleep(10); - } - LOG.info("Synchronization finished"); - } - return null; - } - }); - } - - @Override - public CustomMessage send(InetAddress server, int port, CustomMessage request) { - try (SocketChannel channel = SocketChannel.open(new InetSocketAddress(server, port))) { - channel.configureBlocking(true); - ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_SIZE); - ByteBuffer payloadBuffer = new NetworkMessage(request).writeHeaderAndGetPayloadBuffer(headerBuffer); - headerBuffer.flip(); - while (headerBuffer.hasRemaining()) { - channel.write(headerBuffer); - } - while (payloadBuffer.hasRemaining()) { - channel.write(payloadBuffer); - } - - V3MessageReader reader = new V3MessageReader(); - while (channel.isConnected() && reader.getMessages().isEmpty()) { - if (channel.read(reader.getActiveBuffer()) > 0) { - reader.update(); - } else { - throw new NodeException("No response from node " + server); - } - } - NetworkMessage networkMessage; - if (reader.getMessages().isEmpty()) { - throw new NodeException("No response from node " + server); - } else { - networkMessage = reader.getMessages().get(0); - } - - if (networkMessage != null && networkMessage.getPayload() instanceof CustomMessage) { - return (CustomMessage) networkMessage.getPayload(); - } else { - if (networkMessage == null || networkMessage.getPayload() == null) { - throw new NodeException("Empty response from node " + server); - } else { - throw new NodeException("Unexpected response from node " + server + ": " - + networkMessage.getPayload().getClass()); - } - } - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - @Override - public void start() { - if (selector != null && selector.isOpen()) { - throw new IllegalStateException("Network already running - you need to stop first."); - } - try { - selector = Selector.open(); - } catch (IOException e) { - throw new ApplicationException(e); - } - requestedObjects.clear(); - - starter = thread("connection manager", new Runnable() { - @Override - public void run() { - while (selector.isOpen()) { - int missing = NETWORK_MAGIC_NUMBER; - for (ConnectionInfo connectionInfo : connections.keySet()) { - if (connectionInfo.getState() == ACTIVE) { - missing--; - if (missing == 0) break; - } - } - if (missing > 0) { - List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses(100, ctx.getStreams()); - addresses = selectRandom(missing, addresses); - for (NetworkAddress address : addresses) { - if (!isConnectedTo(address)) { - connectionQueue.offer(address); - } - } - } - - Iterator<Map.Entry<ConnectionInfo, SelectionKey>> it = connections.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry<ConnectionInfo, SelectionKey> e = it.next(); - if (!e.getValue().isValid() || e.getKey().isExpired()) { - try { - e.getValue().channel().close(); - } catch (Exception ignore) { - } - e.getValue().cancel(); - e.getValue().attach(null); - e.getKey().disconnect(); - it.remove(); - } - } - - // The list 'requested objects' helps to prevent downloading an object - // twice. From time to time there is an error though, and an object is - // never downloaded. To prevent a large list of failed objects and give - // them a chance to get downloaded again, we will attempt to download an - // object from another node after some time out. - long timedOut = System.currentTimeMillis() - REQUESTED_OBJECTS_MAX_TIME; - List<InventoryVector> delayed = new LinkedList<>(); - Iterator<Map.Entry<InventoryVector, Long>> iterator = requestedObjects.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry<InventoryVector, Long> e = iterator.next(); - //noinspection NumberEquality - if (e.getValue() == DELAYED) { - iterator.remove(); - } else if (e.getValue() < timedOut) { - delayed.add(e.getKey()); - e.setValue(DELAYED); - } - } - request(delayed); - - try { - Thread.sleep(30_000); - } catch (InterruptedException e) { - return; - } - } - } - }); - - thread("selector worker", new Runnable() { - @Override - public void run() { - try { - serverChannel = ServerSocketChannel.open(); - serverChannel.configureBlocking(false); - serverChannel.socket().bind(new InetSocketAddress(ctx.getPort())); - serverChannel.register(selector, OP_ACCEPT, null); - - while (selector.isOpen()) { - selector.select(1000); - Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - keyIterator.remove(); - if (key.attachment() == null) { - try { - if (key.isAcceptable()) { - // handle accept - try { - SocketChannel accepted = ((ServerSocketChannel) key.channel()).accept(); - accepted.configureBlocking(false); - ConnectionInfo connection = new ConnectionInfo(ctx, SERVER, - new NetworkAddress.Builder() - .ip(accepted.socket().getInetAddress()) - .port(accepted.socket().getPort()) - .stream(1) - .build(), - requestedObjects, 0 - ); - connections.put( - connection, - accepted.register(selector, OP_READ | OP_WRITE, connection) - ); - } catch (AsynchronousCloseException e) { - LOG.trace(e.getMessage()); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - } - } - } catch (CancelledKeyException e) { - LOG.debug(e.getMessage(), e); - } - } else { - // handle read/write - SocketChannel channel = (SocketChannel) key.channel(); - ConnectionInfo connection = (ConnectionInfo) key.attachment(); - try { - if (key.isConnectable()) { - if (!channel.finishConnect()) { - continue; - } - } - if (key.isWritable()) { - write(channel, connection); - } - if (key.isReadable()) { - read(channel, connection); - } - if (connection.getState() == DISCONNECTED) { - key.interestOps(0); - channel.close(); - } else if (connection.isWritePending()) { - key.interestOps(OP_READ | OP_WRITE); - } else { - key.interestOps(OP_READ); - } - } catch (CancelledKeyException | NodeException | IOException e) { - connection.disconnect(); - } - } - } - // set interest ops - for (Map.Entry<ConnectionInfo, SelectionKey> e : connections.entrySet()) { - try { - if (e.getValue().isValid() - && (e.getValue().interestOps() & OP_WRITE) == 0 - && (e.getValue().interestOps() & OP_CONNECT) == 0 - && !e.getKey().getSendingQueue().isEmpty()) { - e.getValue().interestOps(OP_READ | OP_WRITE); - } - } catch (CancelledKeyException x) { - e.getKey().disconnect(); - } - } - // start new connections - if (!connectionQueue.isEmpty()) { - NetworkAddress address = connectionQueue.poll(); - try { - SocketChannel channel = SocketChannel.open(); - channel.configureBlocking(false); - channel.connect(new InetSocketAddress(address.toInetAddress(), address.getPort())); - ConnectionInfo connection = new ConnectionInfo(ctx, CLIENT, - address, - requestedObjects, 0 - ); - connections.put( - connection, - channel.register(selector, OP_CONNECT, connection) - ); - } catch (NoRouteToHostException ignore) { - // We'll try to connect to many offline nodes, so - // this is expected to happen quite a lot. - } catch (AsynchronousCloseException e) { - // The exception is expected if the network is being - // shut down, as we actually do asynchronously close - // the connections. - if (isRunning()) { - LOG.error(e.getMessage(), e); - } - } catch (IOException e) { - LOG.error(e.getMessage(), e); - } - } - } - selector.close(); - } catch (ClosedSelectorException ignore) { - } catch (IOException e) { - throw new ApplicationException(e); - } - } - }); - } - - private static void write(SocketChannel channel, ConnectionInfo connection) - throws IOException { - writeBuffer(connection.getOutBuffers(), channel); - - connection.updateWriter(); - - writeBuffer(connection.getOutBuffers(), channel); - connection.cleanupBuffers(); - } - - private static void writeBuffer(ByteBuffer[] buffers, SocketChannel channel) throws IOException { - if (buffers[1] == null) { - if (buffers[0].hasRemaining()) { - channel.write(buffers[0]); - } - } else if (buffers[1].hasRemaining() || buffers[0].hasRemaining()) { - channel.write(buffers); - } - } - - private static void read(SocketChannel channel, ConnectionInfo connection) throws IOException { - if (channel.read(connection.getInBuffer()) > 0) { - connection.updateReader(); - } - connection.updateSyncStatus(); - } - - private Thread thread(String threadName, Runnable runnable) { - Thread thread = new Thread(runnable, threadName); - thread.setDaemon(true); - thread.setPriority(Thread.MIN_PRIORITY); - thread.start(); - return thread; - } - - @Override - public void stop() { - try { - serverChannel.socket().close(); - selector.close(); - for (SelectionKey selectionKey : connections.values()) { - selectionKey.channel().close(); - } - } catch (IOException e) { - throw new ApplicationException(e); - } - } - - @Override - public void offer(InventoryVector iv) { - List<ConnectionInfo> target = new LinkedList<>(); - for (ConnectionInfo connection : connections.keySet()) { - if (connection.getState() == ACTIVE && !connection.knowsOf(iv)) { - target.add(connection); - } - } - List<ConnectionInfo> randomSubset = selectRandom(NETWORK_MAGIC_NUMBER, target); - for (ConnectionInfo connection : randomSubset) { - connection.offer(iv); - } - } - - @Override - public void request(Collection<InventoryVector> inventoryVectors) { - if (!isRunning()) { - requestedObjects.clear(); - return; - } - Iterator<InventoryVector> iterator = inventoryVectors.iterator(); - if (!iterator.hasNext()) { - return; - } - - Map<ConnectionInfo, List<InventoryVector>> distribution = new HashMap<>(); - for (ConnectionInfo connection : connections.keySet()) { - if (connection.getState() == ACTIVE) { - distribution.put(connection, new LinkedList<InventoryVector>()); - } - } - if (distribution.isEmpty()) { - return; - } - InventoryVector next = iterator.next(); - ConnectionInfo previous = null; - do { - for (ConnectionInfo connection : distribution.keySet()) { - if (connection == previous || previous == null) { - if (iterator.hasNext()) { - previous = connection; - next = iterator.next(); - } else { - break; - } - } - if (connection.knowsOf(next) && !connection.requested(next)) { - List<InventoryVector> ivs = distribution.get(connection); - if (ivs.size() == GetData.MAX_INVENTORY_SIZE) { - connection.send(new GetData.Builder().inventory(ivs).build()); - ivs.clear(); - } - ivs.add(next); - iterator.remove(); - - if (iterator.hasNext()) { - next = iterator.next(); - previous = connection; - } else { - break; - } - } - } - } while (iterator.hasNext()); - - // remove objects nobody knows of - for (InventoryVector iv : inventoryVectors) { - requestedObjects.remove(iv); - } - - for (ConnectionInfo connection : distribution.keySet()) { - List<InventoryVector> ivs = distribution.get(connection); - if (!ivs.isEmpty()) { - connection.send(new GetData.Builder().inventory(ivs).build()); - } - } - } - - @Override - public Property getNetworkStatus() { - TreeSet<Long> streams = new TreeSet<>(); - TreeMap<Long, Integer> incomingConnections = new TreeMap<>(); - TreeMap<Long, Integer> outgoingConnections = new TreeMap<>(); - - for (ConnectionInfo connection : connections.keySet()) { - if (connection.getState() == ACTIVE) { - for (long stream : connection.getStreams()) { - 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", isRunning() ? "running" : "stopped"), - new Property("connections", null, streamProperties), - new Property("requestedObjects", requestedObjects.size()) - ); - } - - private boolean isConnectedTo(NetworkAddress address) { - for (ConnectionInfo c : connections.keySet()) { - if (c.getNode().equals(address)) { - return true; - } - } - return false; - } - - @Override - public boolean isRunning() { - return selector != null && selector.isOpen() && starter.isAlive(); - } - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } -} diff --git a/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/Connection.kt b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/Connection.kt new file mode 100644 index 0000000..22293c9 --- /dev/null +++ b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/Connection.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2017 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.nio + +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES +import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE +import ch.dissem.bitmessage.entity.* +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException +import ch.dissem.bitmessage.ports.NetworkHandler +import ch.dissem.bitmessage.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.UnixTime +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import org.slf4j.LoggerFactory +import java.io.IOException +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Contains everything used by both the old streams-oriented NetworkHandler and the new NioNetworkHandler, + * respectively their connection objects. + */ +class Connection( + private val ctx: InternalContext, + val mode: Mode, + val node: NetworkAddress, + private val commonRequestedObjects: MutableMap<InventoryVector, Long>, + syncTimeout: Long +) { + private val requestedObjects: MutableSet<InventoryVector> = Collections.newSetFromMap(ConcurrentHashMap<InventoryVector, Boolean>(10000)) + + internal val io = ConnectionIO(mode, syncTimeout, commonRequestedObjects, requestedObjects, { state }, this::handleMessage) + private var initializer: NetworkConnectionInitializer? = NetworkConnectionInitializer(ctx, node, mode, io::send) { s -> + state = State.ACTIVE + streams = s + initializer = null + } + + private val listener: NetworkHandler.MessageListener = ctx.networkListener + private val ivCache: MutableMap<InventoryVector, Long> = ConcurrentHashMap() + + private var lastObjectTime: Long = 0 + + lateinit var streams: LongArray + protected set + + @Volatile var state = State.CONNECTING + private set + + val isSyncFinished + get() = io.isSyncFinished + + val nothingToSend + get() = io.sendingQueue.isEmpty() + + init { + initializer!!.start() + } + + fun send(payload: MessagePayload) = io.send(payload) + + private fun handleMessage(payload: MessagePayload) { + when (state) { + State.CONNECTING -> initializer!!.handleCommand(payload) + State.ACTIVE -> receiveMessage(payload) + State.DISCONNECTED -> disconnect() + } + } + + private fun receiveMessage(messagePayload: MessagePayload) { + when (messagePayload.command) { + MessagePayload.Command.INV -> receiveMessage(messagePayload as Inv) + MessagePayload.Command.GETDATA -> receiveMessage(messagePayload as GetData) + MessagePayload.Command.OBJECT -> receiveMessage(messagePayload as ObjectMessage) + MessagePayload.Command.ADDR -> receiveMessage(messagePayload as Addr) + else -> throw IllegalStateException("Unexpectedly received '${messagePayload.command}' command") + } + } + + private fun receiveMessage(inv: Inv) { + val originalSize = inv.inventory.size + updateIvCache(inv.inventory) + val missing = ctx.inventory.getMissing(inv.inventory, *streams) + LOG.trace("Received inventory with $originalSize elements, of which are ${missing.size} missing.") + io.send(GetData(missing - commonRequestedObjects.keys)) + } + + private fun receiveMessage(getData: GetData) { + getData.inventory.forEach { iv -> ctx.inventory.getObject(iv)?.let { obj -> io.send(obj) } } + } + + private fun receiveMessage(objectMessage: ObjectMessage) { + requestedObjects.remove(objectMessage.inventoryVector) + if (ctx.inventory.contains(objectMessage)) { + LOG.trace("Received object ${objectMessage.inventoryVector} - already in inventory") + return + } + try { + listener.receive(objectMessage) + cryptography().checkProofOfWork(objectMessage, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES) + ctx.inventory.storeObject(objectMessage) + // offer object to some random nodes so it gets distributed throughout the network: + ctx.networkHandler.offer(objectMessage.inventoryVector) + lastObjectTime = UnixTime.now + } catch (e: InsufficientProofOfWorkException) { + LOG.warn(e.message) + // DebugUtils.saveToFile(objectMessage); // this line must not be committed active + } catch (e: IOException) { + LOG.error("Stream ${objectMessage.stream}, object type ${objectMessage.type}: ${e.message}", e) + } finally { + if (commonRequestedObjects.remove(objectMessage.inventoryVector) == null) { + LOG.debug("Received object that wasn't requested.") + } + } + } + + private fun receiveMessage(addr: Addr) { + LOG.trace("Received ${addr.addresses.size} addresses.") + ctx.nodeRegistry.offerAddresses(addr.addresses) + } + + private fun updateIvCache(inventory: List<InventoryVector>) { + cleanupIvCache() + val now = UnixTime.now + for (iv in inventory) { + ivCache.put(iv, now) + } + } + + fun offer(iv: InventoryVector) { + io.send(Inv(listOf(iv))) + updateIvCache(listOf(iv)) + } + + fun knowsOf(iv: InventoryVector): Boolean { + return ivCache.containsKey(iv) + } + + fun requested(iv: InventoryVector): Boolean { + return requestedObjects.contains(iv) + } + + private fun cleanupIvCache() { + val fiveMinutesAgo = UnixTime.now - 5 * MINUTE + for ((key, value) in ivCache) { + if (value < fiveMinutesAgo) { + ivCache.remove(key) + } + } + } + + // the TCP timeout starts out at 20 seconds + // after verack messages are exchanged, the timeout is raised to 10 minutes + fun isExpired(): Boolean = when (state) { + State.CONNECTING -> io.lastUpdate < System.currentTimeMillis() - 20000 + State.ACTIVE -> io.lastUpdate < System.currentTimeMillis() - 600000 + State.DISCONNECTED -> true + } + + fun disconnect() { + state = State.DISCONNECTED + io.disconnect() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Connection) return false + return node == other.node + } + + override fun hashCode(): Int { + return Objects.hash(node) + } + + enum class State { + CONNECTING, ACTIVE, DISCONNECTED + } + + enum class Mode { + SERVER, CLIENT, SYNC + } + + companion object { + private val LOG = LoggerFactory.getLogger(Connection::class.java) + } +} diff --git a/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/ConnectionIO.kt b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/ConnectionIO.kt new file mode 100644 index 0000000..ec926e3 --- /dev/null +++ b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/ConnectionIO.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.networking.nio + +import ch.dissem.bitmessage.entity.GetData +import ch.dissem.bitmessage.entity.MessagePayload +import ch.dissem.bitmessage.entity.NetworkMessage +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.exception.NodeException +import ch.dissem.bitmessage.factory.V3MessageReader +import ch.dissem.bitmessage.utils.UnixTime +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.ConcurrentLinkedDeque + +/** + * Represents the current state of a connection. + */ +class ConnectionIO( + private val mode: Connection.Mode, + syncTimeout: Long, + private val commonRequestedObjects: MutableMap<InventoryVector, Long>, + private val requestedObjects: MutableSet<InventoryVector>, + private val getState: () -> Connection.State, + private val handleMessage: (MessagePayload) -> Unit +) { + private val headerOut: ByteBuffer = ByteBuffer.allocate(24) + private var payloadOut: ByteBuffer? = null + private var reader: V3MessageReader? = V3MessageReader() + internal val sendingQueue: Deque<MessagePayload> = ConcurrentLinkedDeque<MessagePayload>() + + internal var lastUpdate = System.currentTimeMillis() + private set + + private val syncTimeout: Long = if (syncTimeout > 0) UnixTime.now + syncTimeout else 0 + private var syncReadTimeout = java.lang.Long.MAX_VALUE + + init { + headerOut.flip() + } + + val inBuffer: ByteBuffer + get() = reader?.getActiveBuffer() ?: throw NodeException("Node is disconnected") + + fun updateWriter() { + if (!headerOut.hasRemaining() && !sendingQueue.isEmpty()) { + headerOut.clear() + val payload = sendingQueue.poll() + payloadOut = NetworkMessage(payload).writeHeaderAndGetPayloadBuffer(headerOut) + headerOut.flip() + lastUpdate = System.currentTimeMillis() + } + } + + val outBuffers: Array<ByteBuffer> + get() = payloadOut?.let { arrayOf(headerOut, it) } ?: arrayOf(headerOut) + + fun cleanupBuffers() { + payloadOut?.let { + if (!it.hasRemaining()) payloadOut = null + } + } + + fun updateReader() { + reader?.let { reader -> + reader.update() + if (!reader.getMessages().isEmpty()) { + val iterator = reader.getMessages().iterator() + var msg: NetworkMessage? = null + while (iterator.hasNext()) { + msg = iterator.next() + handleMessage(msg.payload) + iterator.remove() + } + isSyncFinished = syncFinished(msg) + } + lastUpdate = System.currentTimeMillis() + } + } + + fun updateSyncStatus() { + if (!isSyncFinished) { + isSyncFinished = reader?.getMessages()?.isEmpty() ?: true && syncFinished(null) + } + } + + protected fun syncFinished(msg: NetworkMessage?): Boolean { + if (mode != Connection.Mode.SYNC) { + return false + } + if (Thread.interrupted() || getState() == Connection.State.DISCONNECTED) { + return true + } + if (getState() == Connection.State.CONNECTING) { + return false + } + if (syncTimeout < UnixTime.now) { + LOG.info("Synchronization timed out") + return true + } + if (!nothingToSend()) { + syncReadTimeout = System.currentTimeMillis() + 1000 + return false + } + if (msg == null) { + return syncReadTimeout < System.currentTimeMillis() + } else { + syncReadTimeout = System.currentTimeMillis() + 1000 + return false + } + } + + fun disconnect() { + reader?.let { + it.cleanup() + reader = null + } + payloadOut = null + } + + fun send(payload: MessagePayload) { + sendingQueue.add(payload) + if (payload is GetData) { + val now = UnixTime.now + val inventory = payload.inventory + requestedObjects.addAll(inventory) + inventory.forEach { iv -> commonRequestedObjects.put(iv, now) } + } + } + + var isSyncFinished = false + + val isWritePending: Boolean + get() = !sendingQueue.isEmpty() + || headerOut.hasRemaining() + || payloadOut?.hasRemaining() ?: false + + fun nothingToSend() = sendingQueue.isEmpty() + + companion object { + val LOG = LoggerFactory.getLogger(ConnectionIO::class.java) + } +} diff --git a/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NetworkConnectionInitializer.kt b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NetworkConnectionInitializer.kt new file mode 100644 index 0000000..df915cd --- /dev/null +++ b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NetworkConnectionInitializer.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2017 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.nio + +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.entity.* +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.exception.NodeException +import ch.dissem.bitmessage.utils.UnixTime +import org.slf4j.LoggerFactory + +/** + * Handles the initialization phase of connection and, due to their design, custom commands. + */ +class NetworkConnectionInitializer( + private val ctx: InternalContext, + val node: NetworkAddress, + val mode: Connection.Mode, + val send: (MessagePayload) -> Unit, + val markActive: (LongArray) -> Unit +) { + private lateinit var version: Version + + private var verackSent: Boolean = false + private var verackReceived: Boolean = false + + fun start() { + if (mode == Connection.Mode.CLIENT || mode == Connection.Mode.SYNC) { + send(Version(nonce = ctx.clientNonce, addrFrom = NetworkAddress.ANY, addrRecv = node, userAgent = ctx.userAgent)) + } + } + + fun handleCommand(payload: MessagePayload) { + when (payload.command) { + MessagePayload.Command.VERSION -> handleVersion(payload as Version) + MessagePayload.Command.VERACK -> { + if (verackSent) { + activateConnection() + } + verackReceived = true + } + MessagePayload.Command.CUSTOM -> { + ctx.customCommandHandler.handle(payload as CustomMessage)?.let { response -> + send(response) + } ?: throw NodeException("No response for custom command available") + } + else -> throw NodeException("Command 'version' or 'verack' expected, but was '${payload.command}'") + } + } + + private fun handleVersion(version: Version) { + if (version.nonce == ctx.clientNonce) { + throw NodeException("Tried to connect to self, disconnecting.") + } else if (version.version >= BitmessageContext.CURRENT_VERSION) { + this.version = version + verackSent = true + send(VerAck()) + if (mode == Connection.Mode.SERVER) { + send(Version.Builder().defaults(ctx.clientNonce).addrFrom(NetworkAddress.ANY).addrRecv(node).build()) + } + if (verackReceived) { + activateConnection() + } + } else { + throw NodeException("Received unsupported version " + version.version + ", disconnecting.") + } + } + + private fun activateConnection() { + LOG.info("Successfully established connection with node " + node) + markActive(version.streams) + node.time = UnixTime.now + if (mode != Connection.Mode.SYNC) { + sendAddresses() + ctx.nodeRegistry.offerAddresses(listOf(node)) + } + sendInventory() + } + + + private fun sendAddresses() { + val addresses = ctx.nodeRegistry.getKnownAddresses(1000, *version.streams) + send(Addr(addresses)) + } + + private fun sendInventory() { + val inventory = ctx.inventory.getInventory(*version.streams) + var i = 0 + while (i < inventory.size) { + send(Inv(inventory.subList(i, Math.min(inventory.size, i + 50000)))) + i += 50000 + } + } + + companion object { + val LOG = LoggerFactory.getLogger(NetworkConnectionInitializer::class.java)!! + } +} diff --git a/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt new file mode 100644 index 0000000..3a0464a --- /dev/null +++ b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt @@ -0,0 +1,476 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.networking.nio + +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.constants.Network.HEADER_SIZE +import ch.dissem.bitmessage.constants.Network.NETWORK_MAGIC_NUMBER +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.V3MessageReader +import ch.dissem.bitmessage.networking.nio.Connection.Mode.* +import ch.dissem.bitmessage.ports.NetworkHandler +import ch.dissem.bitmessage.utils.Collections.selectRandom +import ch.dissem.bitmessage.utils.DebugUtils +import ch.dissem.bitmessage.utils.Property +import ch.dissem.bitmessage.utils.ThreadFactoryBuilder.Companion.pool +import ch.dissem.bitmessage.utils.UnixTime.now +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.NoRouteToHostException +import java.nio.ByteBuffer +import java.nio.channels.* +import java.nio.channels.SelectionKey.* +import java.util.* +import java.util.concurrent.* + +/** + * Network handler using java.nio, resulting in less threads. + */ +class NioNetworkHandler : NetworkHandler, InternalContext.ContextHolder { + + private val threadPool = Executors.newCachedThreadPool( + pool("network") + .lowPrio() + .daemon() + .build()) + + private lateinit var ctx: InternalContext + private var selector: Selector? = null + private var serverChannel: ServerSocketChannel? = null + private val connectionQueue = ConcurrentLinkedQueue<NetworkAddress>() + private val connections = ConcurrentHashMap<Connection, SelectionKey>() + private val requestedObjects = ConcurrentHashMap<InventoryVector, Long>(10000) + + private var starter: Thread? = null + + override fun setContext(context: InternalContext) { + ctx = context + } + + override fun synchronize(server: InetAddress, port: Int, timeoutInSeconds: Long): Future<Void> { + return threadPool.submit(Callable<Void> { + SocketChannel.open(InetSocketAddress(server, port)).use { channel -> + channel.configureBlocking(false) + val connection = Connection(ctx, SYNC, + NetworkAddress.Builder().ip(server).port(port).stream(1).build(), + HashMap<InventoryVector, Long>(), timeoutInSeconds) + while (channel.isConnected && !connection.isSyncFinished) { + write(channel, connection.io) + read(channel, connection.io) + Thread.sleep(10) + } + LOG.info("Synchronization finished") + } + null + }) + } + + override fun send(server: InetAddress, port: Int, request: CustomMessage): CustomMessage { + SocketChannel.open(InetSocketAddress(server, port)).use { channel -> + channel.configureBlocking(true) + val headerBuffer = ByteBuffer.allocate(HEADER_SIZE) + val payloadBuffer = NetworkMessage(request).writeHeaderAndGetPayloadBuffer(headerBuffer) + headerBuffer.flip() + while (headerBuffer.hasRemaining()) { + channel.write(headerBuffer) + } + while (payloadBuffer.hasRemaining()) { + channel.write(payloadBuffer) + } + + val reader = V3MessageReader() + while (channel.isConnected && reader.getMessages().isEmpty()) { + if (channel.read(reader.getActiveBuffer()) > 0) { + reader.update() + } else { + throw NodeException("No response from node $server") + } + } + val networkMessage: NetworkMessage? + if (reader.getMessages().isEmpty()) { + throw NodeException("No response from node " + server) + } else { + networkMessage = reader.getMessages().first() + } + + if (networkMessage.payload is CustomMessage) { + return networkMessage.payload as CustomMessage + } else { + throw NodeException("Unexpected response from node $server: ${networkMessage.payload.javaClass}") + } + } + } + + override fun start() { + if (selector?.isOpen ?: false) { + throw IllegalStateException("Network already running - you need to stop first.") + } + val selector = Selector.open() + this.selector = selector + + requestedObjects.clear() + + starter = thread("connection manager") { + while (selector.isOpen) { + var missing = NETWORK_MAGIC_NUMBER + for (connection in connections.keys) { + if (connection.state == Connection.State.ACTIVE) { + missing-- + if (missing == 0) break + } + } + if (missing > 0) { + var addresses = ctx.nodeRegistry.getKnownAddresses(100, *ctx.streams) + addresses = selectRandom(missing, addresses) + for (address in addresses) { + if (!isConnectedTo(address)) { + connectionQueue.offer(address) + } + } + } + + val it = connections.entries.iterator() + while (it.hasNext()) { + val e = it.next() + if (!e.value.isValid || e.key.isExpired()) { + try { + e.value.channel().close() + } catch (ignore: Exception) { + } + + e.value.cancel() + e.value.attach(null) + e.key.disconnect() + it.remove() + } + } + + // The list 'requested objects' helps to prevent downloading an object + // twice. From time to time there is an error though, and an object is + // never downloaded. To prevent a large list of failed objects and give + // them a chance to get downloaded again, we will attempt to download an + // object from another node after some time out. + val timedOut = System.currentTimeMillis() - REQUESTED_OBJECTS_MAX_TIME + val delayed = mutableListOf<InventoryVector>() + val iterator = requestedObjects.entries.iterator() + while (iterator.hasNext()) { + val e = iterator.next() + + if (e.value == DELAYED) { + iterator.remove() + } else if (e.value < timedOut) { + delayed.add(e.key) + e.setValue(DELAYED) + } + } + request(delayed) + + try { + Thread.sleep(30000) + } catch (e: InterruptedException) { + return@thread + } + } + } + + thread("selector worker", { + try { + val serverChannel = ServerSocketChannel.open() + this.serverChannel = serverChannel + serverChannel.configureBlocking(false) + serverChannel.socket().bind(InetSocketAddress(ctx.port)) + serverChannel.register(selector, OP_ACCEPT, null) + + while (selector.isOpen) { + selector.select(1000) + val keyIterator = selector.selectedKeys().iterator() + while (keyIterator.hasNext()) { + val key = keyIterator.next() + keyIterator.remove() + if (key.attachment() == null) { + try { + if (key.isAcceptable) { + // handle accept + try { + val accepted = (key.channel() as ServerSocketChannel).accept() + accepted.configureBlocking(false) + val connection = Connection(ctx, SERVER, + NetworkAddress( + time = now, + stream = 1L, + socket = accepted.socket()!! + ), + requestedObjects, 0 + ) + connections.put( + connection, + accepted.register(selector, OP_READ or OP_WRITE, connection) + ) + } catch (e: AsynchronousCloseException) { + LOG.trace(e.message) + } catch (e: IOException) { + LOG.error(e.message, e) + } + + } + } catch (e: CancelledKeyException) { + LOG.debug(e.message, e) + } + + } else { + // handle read/write + val channel = key.channel() as SocketChannel + val connection = key.attachment() as Connection + try { + if (key.isConnectable) { + if (!channel.finishConnect()) { + continue + } + } + if (key.isWritable) { + write(channel, connection.io) + } + if (key.isReadable) { + read(channel, connection.io) + } + if (connection.state == Connection.State.DISCONNECTED) { + key.interestOps(0) + channel.close() + } else if (connection.io.isWritePending) { + key.interestOps(OP_READ or OP_WRITE) + } else { + key.interestOps(OP_READ) + } + } catch (e: CancelledKeyException) { + connection.disconnect() + } catch (e: NodeException) { + connection.disconnect() + } catch (e: IOException) { + connection.disconnect() + } + } + } + // set interest ops + for ((connection, selectionKey) in connections) { + try { + if (selectionKey.isValid + && selectionKey.interestOps() and OP_WRITE == 0 + && selectionKey.interestOps() and OP_CONNECT == 0 + && !connection.nothingToSend) { + selectionKey.interestOps(OP_READ or OP_WRITE) + } + } catch (x: CancelledKeyException) { + connection.disconnect() + } + + } + // start new connections + if (!connectionQueue.isEmpty()) { + val address = connectionQueue.poll() + try { + val channel = SocketChannel.open() + channel.configureBlocking(false) + channel.connect(InetSocketAddress(address.toInetAddress(), address.port)) + val connection = Connection(ctx, CLIENT, address, requestedObjects, 0) + connections.put( + connection, + channel.register(selector, OP_CONNECT, connection) + ) + } catch (ignore: NoRouteToHostException) { + // We'll try to connect to many offline nodes, so + // this is expected to happen quite a lot. + } catch (e: AsynchronousCloseException) { + // The exception is expected if the network is being + // shut down, as we actually do asynchronously close + // the connections. + if (isRunning) { + LOG.error(e.message, e) + } + } catch (e: IOException) { + LOG.error(e.message, e) + } + + } + } + selector.close() + } catch (_: ClosedSelectorException) { + } + }) + } + + private fun thread(threadName: String, runnable: () -> Unit): Thread { + val thread = Thread(runnable, threadName) + thread.isDaemon = true + thread.priority = Thread.MIN_PRIORITY + thread.start() + return thread + } + + override fun stop() { + serverChannel?.socket()?.close() + selector?.close() + for (selectionKey in connections.values) { + selectionKey.channel().close() + } + } + + override fun offer(iv: InventoryVector) { + val targetConnections = connections.keys.filter { it.state == Connection.State.ACTIVE && !it.knowsOf(iv) } + selectRandom(NETWORK_MAGIC_NUMBER, targetConnections).forEach { it.offer(iv) } + } + + override fun request(inventoryVectors: MutableCollection<InventoryVector>) { + if (!isRunning) { + requestedObjects.clear() + return + } + val iterator = inventoryVectors.iterator() + if (!iterator.hasNext()) { + return + } + + val distribution = HashMap<Connection, MutableList<InventoryVector>>() + for (connection in connections.keys) { + if (connection.state == Connection.State.ACTIVE) { + distribution.put(connection, mutableListOf<InventoryVector>()) + } + } + if (distribution.isEmpty()) { + return + } + var next = iterator.next() + var previous: Connection? = null + do { + for (connection in distribution.keys) { + if (connection === previous || previous == null) { + if (iterator.hasNext()) { + previous = connection + next = iterator.next() + } else { + break + } + } + if (connection.knowsOf(next) && !connection.requested(next)) { + val ivs = distribution[connection] ?: throw IllegalStateException("distribution not available for $connection") + if (ivs.size == GetData.MAX_INVENTORY_SIZE) { + connection.send(GetData(ivs)) + ivs.clear() + } + ivs.add(next) + iterator.remove() + + if (iterator.hasNext()) { + next = iterator.next() + previous = connection + } else { + break + } + } + } + } while (iterator.hasNext()) + + // remove objects nobody knows of + for (iv in inventoryVectors) { + requestedObjects.remove(iv) + } + + for (connection in distribution.keys) { + val ivs = distribution[connection] ?: throw IllegalStateException("distribution not available for $connection") + if (!ivs.isEmpty()) { + connection.send(GetData(ivs)) + } + } + } + + override fun getNetworkStatus(): Property { + val streams = TreeSet<Long>() + val incomingConnections = TreeMap<Long, Int>() + val outgoingConnections = TreeMap<Long, Int>() + + connections.keys + .filter { it.state == Connection.State.ACTIVE } + .forEach { + for (stream in it.streams) { + streams.add(stream) + if (it.mode == SERVER) { + DebugUtils.inc(incomingConnections, stream) + } else { + DebugUtils.inc(outgoingConnections, stream) + } + } + } + val streamProperties = mutableListOf<Property>() + for (stream in streams) { + val incoming = incomingConnections[stream] ?: 0 + val outgoing = outgoingConnections[stream] ?: 0 + streamProperties.add(Property("stream " + stream, Property("nodes", incoming + outgoing), + Property("incoming", incoming), + Property("outgoing", outgoing) + )) + } + return Property("network", + Property("connectionManager", if (isRunning) "running" else "stopped"), + Property("connections", streamProperties), + Property("requestedObjects", requestedObjects.size) + ) + } + + private fun isConnectedTo(address: NetworkAddress): Boolean { + for (c in connections.keys) { + if (c.node == address) { + return true + } + } + return false + } + + override val isRunning: Boolean + get() = selector?.isOpen ?: false && starter?.isAlive ?: false + + companion object { + private val LOG = LoggerFactory.getLogger(NioNetworkHandler::class.java) + private val REQUESTED_OBJECTS_MAX_TIME = (2 * 60000).toLong() // 2 minutes in ms + private val DELAYED = java.lang.Long.MIN_VALUE + + private fun write(channel: SocketChannel, connection: ConnectionIO) { + writeBuffer(connection.outBuffers, channel) + + connection.updateWriter() + + writeBuffer(connection.outBuffers, channel) + connection.cleanupBuffers() + } + + private fun writeBuffer(buffers: Array<ByteBuffer>, channel: SocketChannel) { + if (buffers.any { buf -> buf.hasRemaining() }) channel.write(buffers) + } + + private fun read(channel: SocketChannel, connection: ConnectionIO) { + if (channel.read(connection.inBuffer) > 0) { + connection.updateReader() + } + connection.updateSyncStatus() + } + } +} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java deleted file mode 100644 index f079ede..0000000 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java +++ /dev/null @@ -1,279 +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.BitmessageContext; -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; -import ch.dissem.bitmessage.entity.CustomMessage; -import ch.dissem.bitmessage.entity.MessagePayload; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.exception.NodeException; -import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; -import ch.dissem.bitmessage.ports.*; -import ch.dissem.bitmessage.testutils.TestInventory; -import ch.dissem.bitmessage.utils.Property; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.DisableOnDebug; -import org.junit.rules.TestRule; -import org.junit.rules.Timeout; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Future; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests network handlers. This test is parametrized, so it can test both the nio and classic implementation - * as well as their combinations. It might be slightly over the top and will most probably be cleaned up once - * the nio implementation is deemed stable. - */ -@RunWith(Parameterized.class) -public class NetworkHandlerTest { - private static final Logger LOG = LoggerFactory.getLogger(NetworkHandlerTest.class); - private static NetworkAddress peerAddress = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(6001).build(); - - private TestInventory peerInventory; - private TestInventory nodeInventory; - - private BitmessageContext peer; - private BitmessageContext node; - - private final NetworkHandler peerNetworkHandler; - private final NetworkHandler nodeNetworkHandler; - - @Rule - public final TestRule timeout = new DisableOnDebug(Timeout.seconds(60)); - - public NetworkHandlerTest(NetworkHandler peer, NetworkHandler node) { - this.peerNetworkHandler = peer; - this.nodeNetworkHandler = node; - } - - @Parameterized.Parameters - @SuppressWarnings("deprecation") - public static List<Object[]> parameters() { - return Arrays.asList(new Object[][]{ - {new DefaultNetworkHandler(), new DefaultNetworkHandler()}, - {new DefaultNetworkHandler(), new NioNetworkHandler()}, - {new NioNetworkHandler(), new DefaultNetworkHandler()}, - {new NioNetworkHandler(), new NioNetworkHandler()} - }); - } - - @Before - public void setUp() throws InterruptedException { - peerInventory = new TestInventory(); - peer = new BitmessageContext.Builder() - .addressRepo(mock(AddressRepository.class)) - .inventory(peerInventory) - .messageRepo(mock(MessageRepository.class)) - .powRepo(mock(ProofOfWorkRepository.class)) - .port(peerAddress.getPort()) - .nodeRegistry(new TestNodeRegistry()) - .networkHandler(peerNetworkHandler) - .cryptography(new BouncyCryptography()) - .listener(mock(BitmessageContext.Listener.class)) - .customCommandHandler(new CustomCommandHandler() { - @Override - public MessagePayload handle(CustomMessage request) { - byte[] data = request.getData(); - if (data.length > 0) { - switch (data[0]) { - case 0: - return null; - case 1: - break; - case 3: - data[0] = 0; - break; - default: - break; - } - } - return new CustomMessage("test response", request.getData()); - } - }) - .build(); - peer.startup(); - Thread.sleep(100); - - nodeInventory = new TestInventory(); - node = new BitmessageContext.Builder() - .addressRepo(mock(AddressRepository.class)) - .inventory(nodeInventory) - .messageRepo(mock(MessageRepository.class)) - .powRepo(mock(ProofOfWorkRepository.class)) - .port(6002) - .nodeRegistry(new TestNodeRegistry(peerAddress)) - .networkHandler(nodeNetworkHandler) - .cryptography(new BouncyCryptography()) - .listener(mock(BitmessageContext.Listener.class)) - .build(); - } - - @After - public void cleanUp() { - shutdown(peer); - shutdown(node); - shutdown(nodeNetworkHandler); - } - - private static void shutdown(BitmessageContext ctx) { - if (!ctx.isRunning()) return; - - ctx.shutdown(); - do { - try { - Thread.sleep(100); - } catch (InterruptedException ignore) { - } - } while (ctx.isRunning()); - } - - private static void shutdown(NetworkHandler networkHandler) { - if (!networkHandler.isRunning()) return; - - networkHandler.stop(); - do { - try { - Thread.sleep(100); - } catch (InterruptedException ignore) { - if (networkHandler.isRunning()) { - LOG.warn("Thread interrupted while waiting for network shutdown - " + - "this could cause problems in subsequent tests."); - } - return; - } - } while (networkHandler.isRunning()); - } - - private Property waitForNetworkStatus(BitmessageContext ctx) throws InterruptedException { - Property status; - do { - Thread.sleep(100); - status = ctx.status().getProperty("network", "connections", "stream 1"); - } while (status == null); - return status; - } - - @Test - public void ensureNodesAreConnecting() throws Exception { - node.startup(); - - Property nodeStatus = waitForNetworkStatus(node); - Property peerStatus = waitForNetworkStatus(peer); - - assertEquals(1, nodeStatus.getProperty("outgoing").getValue()); - assertEquals(1, peerStatus.getProperty("incoming").getValue()); - } - - @Test - public void ensureCustomMessageIsSentAndResponseRetrieved() throws Exception { - byte[] data = cryptography().randomBytes(8); - data[0] = (byte) 1; - CustomMessage request = new CustomMessage("test request", data); - node.startup(); - - CustomMessage response = nodeNetworkHandler.send(peerAddress.toInetAddress(), peerAddress.getPort(), request); - - assertThat(response, notNullValue()); - assertThat(response.getCustomCommand(), is("test response")); - assertThat(response.getData(), is(data)); - } - - @Test(expected = NodeException.class) - public void ensureCustomMessageWithoutResponseYieldsException() throws Exception { - byte[] data = cryptography().randomBytes(8); - data[0] = (byte) 0; - CustomMessage request = new CustomMessage("test request", data); - - CustomMessage response = nodeNetworkHandler.send(peerAddress.toInetAddress(), peerAddress.getPort(), request); - - assertThat(response, notNullValue()); - assertThat(response.getCustomCommand(), is("test response")); - assertThat(response.getData(), is(request.getData())); - } - - @Test - public void ensureObjectsAreSynchronizedIfBothHaveObjects() throws Exception { - peerInventory.init( - "V4Pubkey.payload", - "V5Broadcast.payload" - ); - - nodeInventory.init( - "V1Msg.payload", - "V4Pubkey.payload" - ); - - Future<?> future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.getPort(), 10); - future.get(); - assertInventorySize(3, nodeInventory); - assertInventorySize(3, peerInventory); - } - - @Test - public void ensureObjectsAreSynchronizedIfOnlyPeerHasObjects() throws Exception { - peerInventory.init( - "V4Pubkey.payload", - "V5Broadcast.payload" - ); - - nodeInventory.init(); - - Future<?> future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.getPort(), 10); - future.get(); - assertInventorySize(2, nodeInventory); - assertInventorySize(2, peerInventory); - } - - @Test - public void ensureObjectsAreSynchronizedIfOnlyNodeHasObjects() throws Exception { - peerInventory.init(); - - nodeInventory.init( - "V1Msg.payload" - ); - - Future<?> future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.getPort(), 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/TestNodeRegistry.java b/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java deleted file mode 100644 index fb00b26..0000000 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java +++ /dev/null @@ -1,49 +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.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 void clear() { - // no op - } - - @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/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt new file mode 100644 index 0000000..4eaface --- /dev/null +++ b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt @@ -0,0 +1,266 @@ +/* + * 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.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.CustomMessage +import ch.dissem.bitmessage.entity.MessagePayload +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.exception.NodeException +import ch.dissem.bitmessage.networking.nio.NioNetworkHandler +import ch.dissem.bitmessage.ports.* +import ch.dissem.bitmessage.testutils.TestInventory +import ch.dissem.bitmessage.utils.Property +import ch.dissem.bitmessage.utils.Singleton.cryptography +import com.nhaarman.mockito_kotlin.mock +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.DisableOnDebug +import org.junit.rules.TestRule +import org.junit.rules.Timeout +import org.slf4j.LoggerFactory + +/** + * Tests network handlers. This test is parametrized, so it can test both the nio and classic implementation + * as well as their combinations. It might be slightly over the top and will most probably be cleaned up once + * the nio implementation is deemed stable. + */ +class NetworkHandlerTest { + + private lateinit var peerInventory: TestInventory + private lateinit var nodeInventory: TestInventory + + private lateinit var peer: BitmessageContext + private lateinit var node: BitmessageContext + + private lateinit var peerNetworkHandler: NetworkHandler + private lateinit var nodeNetworkHandler: NetworkHandler + + @JvmField @Rule val timeout: TestRule = DisableOnDebug(Timeout.seconds(60)) + + @Before + fun setUp() { + peerInventory = TestInventory() + peerNetworkHandler = NioNetworkHandler() + peer = BitmessageContext( + cryptography = BouncyCryptography(), + inventory = peerInventory, + nodeRegistry = TestNodeRegistry(), + networkHandler = peerNetworkHandler, + addressRepository = mock<AddressRepository>(), + messageRepository = mock<MessageRepository>(), + proofOfWorkRepository = mock<ProofOfWorkRepository>(), + customCommandHandler = object : CustomCommandHandler { + override fun handle(request: CustomMessage): MessagePayload? { + val data = request.getData() + if (data.isNotEmpty()) { + when (data[0]) { + 0.toByte() -> return null + 1.toByte() -> { + } + 3.toByte() -> data[0] = 0 + } + } + return CustomMessage("test response", request.getData()) + } + }, + listener = mock<BitmessageContext.Listener>(), + port = peerAddress.port + ) + peer.startup() + Thread.sleep(100) + + nodeInventory = TestInventory() + nodeNetworkHandler = NioNetworkHandler() + node = BitmessageContext( + cryptography = BouncyCryptography(), + inventory = nodeInventory, + nodeRegistry = TestNodeRegistry(peerAddress), + networkHandler = nodeNetworkHandler, + addressRepository = mock<AddressRepository>(), + messageRepository = mock<MessageRepository>(), + proofOfWorkRepository = mock<ProofOfWorkRepository>(), + customCommandHandler = object : CustomCommandHandler { + override fun handle(request: CustomMessage): MessagePayload? { + val data = request.getData() + if (data.isNotEmpty()) { + when (data[0]) { + 0.toByte() -> return null + 1.toByte() -> { + } + 3.toByte() -> data[0] = 0 + } + } + return CustomMessage("test response", request.getData()) + } + }, + listener = mock<BitmessageContext.Listener>(), + port = 6002 + ) + } + + @After + fun cleanUp() { + shutdown(peer) + shutdown(node) + shutdown(nodeNetworkHandler) + } + + private fun waitForNetworkStatus(ctx: BitmessageContext): Property { + var status: Property? + do { + Thread.sleep(100) + status = ctx.status().getProperty("network", "connections", "stream 1") + } while (status == null) + return status + } + + @Test + fun `ensure nodes are connecting`() { + node.startup() + + val nodeStatus = waitForNetworkStatus(node) + val peerStatus = waitForNetworkStatus(peer) + + assertEquals(1, nodeStatus.getProperty("outgoing")!!.value) + assertEquals(1, peerStatus.getProperty("incoming")!!.value) + } + + @Test + fun `ensure CustomMessage is sent and response retrieved`() { + val data = cryptography().randomBytes(8) + data[0] = 1.toByte() + val request = CustomMessage("test request", data) + node.startup() + + val response = nodeNetworkHandler.send(peerAddress.toInetAddress(), peerAddress.port, request) + + assertThat(response, notNullValue()) + assertThat(response.customCommand, `is`("test response")) + assertThat(response.getData(), `is`(data)) + } + + @Test(expected = NodeException::class) + fun `ensure CustomMessage without response yields exception`() { + val data = cryptography().randomBytes(8) + data[0] = 0.toByte() + val request = CustomMessage("test request", data) + + val response = nodeNetworkHandler.send(peerAddress.toInetAddress(), peerAddress.port, request) + + assertThat(response, notNullValue()) + assertThat(response.customCommand, `is`("test response")) + assertThat(response.getData(), `is`(request.getData())) + } + + @Test + fun `ensure objects are synchronized if both have objects`() { + peerInventory.init( + "V4Pubkey.payload", + "V5Broadcast.payload" + ) + + nodeInventory.init( + "V1Msg.payload", + "V4Pubkey.payload" + ) + + val future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.port, 10) + future.get() + assertInventorySize(3, nodeInventory) + assertInventorySize(3, peerInventory) + } + + @Test + fun `ensure objects are synchronized if only peer has objects`() { + peerInventory.init( + "V4Pubkey.payload", + "V5Broadcast.payload" + ) + + nodeInventory.init() + + val future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.port, 10) + future.get() + assertInventorySize(2, nodeInventory) + assertInventorySize(2, peerInventory) + } + + @Test + fun `ensure objects are synchronized if only node has objects`() { + peerInventory.init() + + nodeInventory.init( + "V1Msg.payload" + ) + + val future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.port, 10) + future.get() + assertInventorySize(1, nodeInventory) + assertInventorySize(1, peerInventory) + } + + private fun assertInventorySize(expected: Int, inventory: TestInventory) { + val timeout = System.currentTimeMillis() + 1000 + while (expected != inventory.getInventory().size && System.currentTimeMillis() < timeout) { + Thread.sleep(10) + } + assertEquals(expected.toLong(), inventory.getInventory().size.toLong()) + } + + companion object { + private val LOG = LoggerFactory.getLogger(NetworkHandlerTest::class.java) + private val peerAddress = NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(6001).build() + + private fun shutdown(ctx: BitmessageContext) { + if (!ctx.isRunning) return + + ctx.shutdown() + do { + try { + Thread.sleep(100) + } catch (ignore: InterruptedException) { + } + + } while (ctx.isRunning) + } + + private fun shutdown(networkHandler: NetworkHandler) { + if (!networkHandler.isRunning) return + + networkHandler.stop() + do { + try { + Thread.sleep(100) + } catch (ignore: InterruptedException) { + if (networkHandler.isRunning) { + LOG.warn("Thread interrupted while waiting for network shutdown - " + "this could cause problems in subsequent tests.") + } + return + } + + } while (networkHandler.isRunning) + } + } + +} diff --git a/networking/src/test/kotlin/ch/dissem/bitmessage/networking/TestNodeRegistry.kt b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/TestNodeRegistry.kt new file mode 100644 index 0000000..a454c91 --- /dev/null +++ b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/TestNodeRegistry.kt @@ -0,0 +1,41 @@ +/* + * 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 + +/** + * Empty [NodeRegistry] that doesn't do anything, but shouldn't break things either. + */ +internal class TestNodeRegistry(vararg nodes: NetworkAddress) : NodeRegistry { + private val nodes: List<NetworkAddress> = listOf(*nodes) + + override fun clear() { + // no op + } + + override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> { + return nodes + } + + override fun offerAddresses(nodes: List<NetworkAddress>) { + // Ignore + } +} diff --git a/repositories/build.gradle b/repositories/build.gradle index 79b8578..ce3b83a 100644 --- a/repositories/build.gradle +++ b/repositories/build.gradle @@ -10,14 +10,12 @@ uploadArchives { } } -sourceCompatibility = 1.8 - dependencies { compile project(':core') - compile 'org.flywaydb:flyway-core:4.1.2' - testCompile 'junit:junit:4.12' - testCompile 'com.h2database:h2:1.4.194' - testCompile 'org.mockito:mockito-core:2.7.21' + compile 'org.flywaydb:flyway-core' + testCompile 'junit:junit' + testCompile 'com.h2database:h2' + testCompile 'com.nhaarman:mockito-kotlin' testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java deleted file mode 100644 index 5422997..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java +++ /dev/null @@ -1,239 +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.repository; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -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.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.AddressRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.sql.*; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -public class JdbcAddressRepository extends JdbcHelper implements AddressRepository { - private static final Logger LOG = LoggerFactory.getLogger(JdbcAddressRepository.class); - - public JdbcAddressRepository(JdbcConfig config) { - super(config); - } - - @Override - public BitmessageAddress findContact(byte[] ripeOrTag) { - for (BitmessageAddress address : find("public_key is null")) { - if (address.getVersion() > 3) { - if (Arrays.equals(ripeOrTag, address.getTag())) return address; - } else { - if (Arrays.equals(ripeOrTag, address.getRipe())) return address; - } - } - return null; - } - - @Override - public BitmessageAddress findIdentity(byte[] ripeOrTag) { - for (BitmessageAddress address : find("private_key is not null")) { - if (address.getVersion() > 3) { - if (Arrays.equals(ripeOrTag, address.getTag())) return address; - } else { - if (Arrays.equals(ripeOrTag, address.getRipe())) return address; - } - } - return null; - } - - @Override - public List<BitmessageAddress> getIdentities() { - return find("private_key IS NOT NULL"); - } - - @Override - public List<BitmessageAddress> getChans() { - return find("chan = '1'"); - } - - @Override - public List<BitmessageAddress> getSubscriptions() { - return find("subscribed = '1'"); - } - - @Override - public List<BitmessageAddress> getSubscriptions(long broadcastVersion) { - if (broadcastVersion > 4) { - return find("subscribed = '1' AND version > 3"); - } else { - return find("subscribed = '1' AND version <= 3"); - } - } - - @Override - public List<BitmessageAddress> getContacts() { - return find("private_key IS NULL OR chan = '1'"); - } - - private List<BitmessageAddress> find(String where) { - List<BitmessageAddress> result = new LinkedList<>(); - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed, chan " + - "FROM Address WHERE " + where) - ) { - while (rs.next()) { - BitmessageAddress address; - - InputStream privateKeyStream = rs.getBinaryStream("private_key"); - if (privateKeyStream == null) { - address = new BitmessageAddress(rs.getString("address")); - Blob publicKeyBlob = rs.getBlob("public_key"); - if (publicKeyBlob != null) { - Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(), - publicKeyBlob.getBinaryStream(), (int) publicKeyBlob.length(), false); - if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) { - pubkey = new V4Pubkey((V3Pubkey) pubkey); - } - address.setPubkey(pubkey); - } - } else { - PrivateKey privateKey = PrivateKey.read(privateKeyStream); - address = new BitmessageAddress(privateKey); - } - address.setAlias(rs.getString("alias")); - address.setSubscribed(rs.getBoolean("subscribed")); - address.setChan(rs.getBoolean("chan")); - - result.add(address); - } - } catch (IOException | SQLException e) { - LOG.error(e.getMessage(), e); - } - return result; - } - - private boolean exists(BitmessageAddress address) { - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Address " + - "WHERE address='" + address.getAddress() + "'") - ) { - if (rs.next()) { - return rs.getInt(1) > 0; - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - return false; - } - - @Override - public void save(BitmessageAddress address) { - try { - if (exists(address)) { - update(address); - } else { - insert(address); - } - } catch (IOException | SQLException e) { - LOG.error(e.getMessage(), e); - } - } - - private void update(BitmessageAddress address) throws IOException, SQLException { - StringBuilder statement = new StringBuilder("UPDATE Address SET alias=?"); - if (address.getPubkey() != null) { - statement.append(", public_key=?"); - } - if (address.getPrivateKey() != null) { - statement.append(", private_key=?"); - } - statement.append(", subscribed=?, chan=? WHERE address=?"); - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement(statement.toString()) - ) { - int i = 0; - ps.setString(++i, address.getAlias()); - if (address.getPubkey() != null) { - writePubkey(ps, ++i, address.getPubkey()); - } - if (address.getPrivateKey() != null) { - writeBlob(ps, ++i, address.getPrivateKey()); - } - ps.setBoolean(++i, address.isSubscribed()); - ps.setBoolean(++i, address.isChan()); - ps.setString(++i, address.getAddress()); - ps.executeUpdate(); - } - } - - private void insert(BitmessageAddress address) throws IOException, SQLException { - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Address (address, version, alias, public_key, private_key, subscribed, chan) " + - "VALUES (?, ?, ?, ?, ?, ?, ?)") - ) { - ps.setString(1, address.getAddress()); - ps.setLong(2, address.getVersion()); - ps.setString(3, address.getAlias()); - writePubkey(ps, 4, address.getPubkey()); - writeBlob(ps, 5, address.getPrivateKey()); - ps.setBoolean(6, address.isSubscribed()); - ps.setBoolean(7, address.isChan()); - ps.executeUpdate(); - } - } - - protected void writePubkey(PreparedStatement ps, int parameterIndex, Pubkey data) throws SQLException, IOException { - if (data != null) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - data.writeUnencrypted(out); - ps.setBytes(parameterIndex, out.toByteArray()); - } else { - ps.setBytes(parameterIndex, null); - } - } - - @Override - public void remove(BitmessageAddress address) { - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement() - ) { - stmt.executeUpdate("DELETE FROM Address WHERE address = '" + address.getAddress() + "'"); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } - - @Override - public BitmessageAddress getAddress(String address) { - List<BitmessageAddress> result = find("address = '" + address + "'"); - if (result.size() > 0) return result.get(0); - return 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 deleted file mode 100644 index 7448b19..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcConfig.java +++ /dev/null @@ -1,51 +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.repository; - -import org.flywaydb.core.Flyway; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; - -/** - * The base configuration for all JDBC based repositories. You should only make one instance, - * as flyway initializes/updates the database at object creation. - */ -public class JdbcConfig { - protected final Flyway flyway; - protected final String dbUrl; - protected final String dbUser; - protected final String dbPassword; - - public JdbcConfig(String dbUrl, String dbUser, String dbPassword) { - this.dbUrl = dbUrl; - this.dbUser = dbUser; - this.dbPassword = dbPassword; - this.flyway = new Flyway(); - flyway.setDataSource(dbUrl, dbUser, dbPassword); - flyway.migrate(); - } - - public JdbcConfig() { - this("jdbc:h2:~/jabit;AUTO_SERVER=TRUE", "sa", null); - } - - public Connection getConnection() throws SQLException { - return DriverManager.getConnection(dbUrl, dbUser, dbPassword); - } -} diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java deleted file mode 100644 index b0be9c1..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java +++ /dev/null @@ -1,54 +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.repository; - -import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.exception.ApplicationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.Collection; - -import static ch.dissem.bitmessage.utils.Strings.hex; - -/** - * Helper class that does Flyway migration, provides JDBC connections and some helper methods. - */ -public abstract class JdbcHelper { - private static final Logger LOG = LoggerFactory.getLogger(JdbcHelper.class); - - protected final JdbcConfig config; - - protected JdbcHelper(JdbcConfig config) { - this.config = config; - } - - public static void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException { - if (data == null) { - ps.setBytes(parameterIndex, null); - } else { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - data.write(os); - ps.setBytes(parameterIndex, os.toByteArray()); - } - } -} diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java deleted file mode 100644 index 33b7290..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java +++ /dev/null @@ -1,187 +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.repository; - -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.ObjectType; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.Inventory; -import org.slf4j.Logger; -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.SqlStrings.join; -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); - } - - @Override - public List<InventoryVector> getInventory(long... streams) { - List<InventoryVector> result = new LinkedList<>(); - for (long stream : streams) { - getCache(stream).entrySet().stream() - .filter(e -> e.getValue() > now()) - .forEach(e -> result.add(e.getKey())); - } - return result; - } - - 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(InventoryVector.fromHash(rs.getBytes("hash")), rs.getLong("expires")); - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } - } - } - return result; - } - - @Override - public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { - for (long stream : streams) { - offer.removeAll(getCache(stream).keySet()); - } - return offer; - } - - @Override - public ObjectMessage getObject(InventoryVector vector) { - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT data, version FROM Inventory WHERE hash = X'" + vector + "'") - ) { - if (rs.next()) { - Blob data = rs.getBlob("data"); - return Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()); - } else { - LOG.info("Object requested that we don't have. IV: " + vector); - return null; - } - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - } - - @Override - public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) { - StringBuilder query = new StringBuilder("SELECT data, version FROM Inventory WHERE 1=1"); - if (stream > 0) { - query.append(" AND stream = ").append(stream); - } - if (version > 0) { - query.append(" AND version = ").append(version); - } - if (types.length > 0) { - query.append(" AND type IN (").append(join(types)).append(')'); - } - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(query.toString()) - ) { - List<ObjectMessage> result = new LinkedList<>(); - while (rs.next()) { - Blob data = rs.getBlob("data"); - result.add(Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length())); - } - return result; - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - } - - @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(); - LOG.trace("Storing object " + iv); - ps.setBytes(1, iv.getHash()); - ps.setLong(2, object.getStream()); - ps.setLong(3, object.getExpiresTime()); - writeBlob(ps, 4, object); - 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) { - LOG.error(e.getMessage(), e); - } - } - - @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(); - Statement stmt = connection.createStatement() - ) { - stmt.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 deleted file mode 100644 index 11c7028..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ /dev/null @@ -1,344 +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.repository; - -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.ports.AbstractMessageRepository; -import ch.dissem.bitmessage.ports.MessageRepository; -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.LinkedList; -import java.util.List; -import java.util.UUID; - -import static ch.dissem.bitmessage.repository.JdbcHelper.writeBlob; - -public class JdbcMessageRepository extends AbstractMessageRepository implements MessageRepository { - private static final Logger LOG = LoggerFactory.getLogger(JdbcMessageRepository.class); - - private final JdbcConfig config; - - public JdbcMessageRepository(JdbcConfig config) { - this.config = config; - } - - @Override - protected List<Label> findLabels(String where) { - try ( - Connection connection = config.getConnection() - ) { - return findLabels(connection, where); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - return new ArrayList<>(); - } - - private Label getLabel(ResultSet rs) throws SQLException { - String typeName = rs.getString("type"); - Label.Type type = null; - if (typeName != null) { - type = Label.Type.valueOf(typeName); - } - Label label = new Label(rs.getString("label"), type, rs.getInt("color")); - label.setId(rs.getLong("id")); - - return label; - } - - @Override - public int countUnread(Label label) { - String where; - if (label == null) { - where = ""; - } else { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND "; - } - where += "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + - "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))"; - - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where) - ) { - if (rs.next()) { - return rs.getInt(1); - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - return 0; - } - - @Override - protected List<Plaintext> find(String where) { - List<Plaintext> result = new LinkedList<>(); - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try, conversation " + - "FROM Message WHERE " + where) - ) { - while (rs.next()) { - byte[] iv = rs.getBytes("iv"); - InputStream data = rs.getBinaryStream("data"); - Plaintext.Type type = Plaintext.Type.valueOf(rs.getString("type")); - Plaintext.Builder builder = Plaintext.readWithoutSignature(type, data); - long id = rs.getLong("id"); - builder.id(id); - builder.IV(InventoryVector.fromHash(iv)); - builder.from(ctx.getAddressRepository().getAddress(rs.getString("sender"))); - builder.to(ctx.getAddressRepository().getAddress(rs.getString("recipient"))); - builder.ackData(rs.getBytes("ack_data")); - builder.sent(rs.getObject("sent", Long.class)); - builder.received(rs.getObject("received", Long.class)); - builder.status(Plaintext.Status.valueOf(rs.getString("status"))); - builder.ttl(rs.getLong("ttl")); - builder.retries(rs.getInt("retries")); - builder.nextTry(rs.getObject("next_try", Long.class)); - builder.conversation(rs.getObject("conversation", UUID.class)); - builder.labels(findLabels(connection, - "id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ") ORDER BY ord")); - Plaintext message = builder.build(); - message.setInitialHash(rs.getBytes("initial_hash")); - result.add(message); - } - } catch (IOException | SQLException e) { - LOG.error(e.getMessage(), e); - } - return result; - } - - private List<Label> findLabels(Connection connection, String where) { - List<Label> result = new ArrayList<>(); - try ( - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE " + where) - ) { - while (rs.next()) { - result.add(getLabel(rs)); - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - return result; - } - - @Override - public void save(Plaintext message) { - saveContactIfNecessary(message.getFrom()); - saveContactIfNecessary(message.getTo()); - - try (Connection connection = config.getConnection()) { - try { - connection.setAutoCommit(false); - save(connection, message); - updateParents(connection, message); - updateLabels(connection, message); - connection.commit(); - } catch (IOException | SQLException e) { - connection.rollback(); - throw e; - } - } catch (IOException | SQLException e) { - throw new ApplicationException(e); - } - } - - private void save(Connection connection, Plaintext message) throws IOException, SQLException { - if (message.getId() == null) { - insert(connection, message); - } else { - update(connection, message); - } - } - - private void updateLabels(Connection connection, Plaintext message) throws SQLException { - // remove existing labels - try (Statement stmt = connection.createStatement()) { - stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id=" + message.getId()); - } - // save new labels - try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Label VALUES (" + - message.getId() + ", ?)")) { - for (Label label : message.getLabels()) { - ps.setLong(1, (Long) label.getId()); - ps.executeUpdate(); - } - } - } - - private void updateParents(Connection connection, Plaintext message) throws SQLException { - if (message.getInventoryVector() == null || message.getParents().isEmpty()) { - // There are no parents to save yet (they are saved in the extended data, that's enough for now) - return; - } - // remove existing parents - try (PreparedStatement ps = connection.prepareStatement("DELETE FROM Message_Parent WHERE child=?")) { - ps.setBytes(1, message.getInitialHash()); - ps.executeUpdate(); - } - byte[] childIV = message.getInventoryVector().getHash(); - // save new parents - int order = 0; - try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Parent VALUES (?, ?, ?, ?)")) { - for (InventoryVector parentIV : message.getParents()) { - Plaintext parent = getMessage(parentIV); - mergeConversations(connection, parent.getConversationId(), message.getConversationId()); - order++; - ps.setBytes(1, parentIV.getHash()); - ps.setBytes(2, childIV); - ps.setInt(3, order); // FIXME: this might not be necessary - ps.setObject(4, message.getConversationId()); - ps.executeUpdate(); - } - } - } - - private void insert(Connection connection, Plaintext message) throws SQLException, IOException { - try (PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Message (iv, type, sender, recipient, data, ack_data, sent, received, " + - "status, initial_hash, ttl, retries, next_try, conversation) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - Statement.RETURN_GENERATED_KEYS) - ) { - ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); - ps.setString(2, message.getType().name()); - ps.setString(3, message.getFrom().getAddress()); - ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress()); - writeBlob(ps, 5, message); - ps.setBytes(6, message.getAckData()); - ps.setObject(7, message.getSent()); - ps.setObject(8, message.getReceived()); - ps.setString(9, message.getStatus() == null ? null : message.getStatus().name()); - ps.setBytes(10, message.getInitialHash()); - ps.setLong(11, message.getTTL()); - ps.setInt(12, message.getRetries()); - ps.setObject(13, message.getNextTry()); - ps.setObject(14, message.getConversationId()); - - ps.executeUpdate(); - // get generated id - try (ResultSet rs = ps.getGeneratedKeys()) { - rs.next(); - message.setId(rs.getLong(1)); - } - } - } - - private void update(Connection connection, Plaintext message) throws SQLException, IOException { - try (PreparedStatement ps = connection.prepareStatement( - "UPDATE Message SET iv=?, type=?, sender=?, recipient=?, data=?, ack_data=?, sent=?, received=?, " + - "status=?, initial_hash=?, ttl=?, retries=?, next_try=? " + - "WHERE id=?")) { - ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); - ps.setString(2, message.getType().name()); - ps.setString(3, message.getFrom().getAddress()); - ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress()); - writeBlob(ps, 5, message); - ps.setBytes(6, message.getAckData()); - ps.setObject(7, message.getSent()); - ps.setObject(8, message.getReceived()); - ps.setString(9, message.getStatus() == null ? null : message.getStatus().name()); - ps.setBytes(10, message.getInitialHash()); - ps.setLong(11, message.getTTL()); - ps.setInt(12, message.getRetries()); - ps.setObject(13, message.getNextTry()); - ps.setLong(14, (Long) message.getId()); - ps.executeUpdate(); - } - } - - @Override - public void remove(Plaintext message) { - try (Connection connection = config.getConnection()) { - connection.setAutoCommit(false); - try (Statement stmt = connection.createStatement()) { - stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id = " + message.getId()); - stmt.executeUpdate("DELETE FROM Message WHERE id = " + message.getId()); - connection.commit(); - } 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); - } - } - - @Override - public List<UUID> findConversations(Label label) { - String where; - if (label == null) { - where = "id NOT IN (SELECT message_id FROM Message_Label)"; - } else { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"; - } - List<UUID> result = new LinkedList<>(); - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT DISTINCT conversation FROM Message WHERE " + where) - ) { - while (rs.next()) { - result.add((UUID) rs.getObject(1)); - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - return result; - } - - /** - * Replaces every occurrence of the source conversation ID with the target ID - * - * @param source ID of the conversation to be merged - * @param target ID of the merge target - */ - private void mergeConversations(Connection connection, UUID source, UUID target) { - try ( - PreparedStatement ps1 = connection.prepareStatement( - "UPDATE Message SET conversation=? WHERE conversation=?"); - PreparedStatement ps2 = connection.prepareStatement( - "UPDATE Message_Parent SET conversation=? WHERE conversation=?") - ) { - ps1.setObject(1, target); - ps1.setObject(2, source); - ps1.executeUpdate(); - ps2.setObject(1, target); - ps2.setObject(2, source); - ps2.executeUpdate(); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } -} diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java deleted file mode 100644 index a889db6..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java +++ /dev/null @@ -1,185 +0,0 @@ -package ch.dissem.bitmessage.repository; - -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.ports.NodeRegistry; -import ch.dissem.bitmessage.utils.Collections; -import ch.dissem.bitmessage.utils.SqlStrings; -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 java.util.Map; -import java.util.Set; - -import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes; -import static ch.dissem.bitmessage.utils.UnixTime.*; - -public class JdbcNodeRegistry extends JdbcHelper implements NodeRegistry { - private static final Logger LOG = LoggerFactory.getLogger(JdbcNodeRegistry.class); - private Map<Long, Set<NetworkAddress>> stableNodes; - - public JdbcNodeRegistry(JdbcConfig config) { - super(config); - cleanUp(); - } - - private void cleanUp() { - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement( - "DELETE FROM Node WHERE time<?") - ) { - ps.setLong(1, now(-28 * DAY)); - ps.executeUpdate(); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } - - private NetworkAddress loadExisting(NetworkAddress node) { - String query = - "SELECT stream, address, port, services, time" + - " FROM Node" + - " WHERE stream = " + node.getStream() + - " AND address = X'" + Strings.hex(node.getIPv6()) + "'" + - " AND port = " + node.getPort(); - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(query) - ) { - if (rs.next()) { - return new NetworkAddress.Builder() - .stream(rs.getLong("stream")) - .ipv6(rs.getBytes("address")) - .port(rs.getInt("port")) - .services(rs.getLong("services")) - .time(rs.getLong("time")) - .build(); - } else { - return null; - } - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - } - - @Override - public void clear() { - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement( - "DELETE FROM Node") - ) { - ps.executeUpdate(); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } - - @Override - public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { - List<NetworkAddress> result = new LinkedList<>(); - String query = - "SELECT stream, address, port, services, time" + - " FROM Node WHERE stream IN (" + SqlStrings.join(streams) + ")" + - " ORDER BY TIME DESC" + - " LIMIT " + limit; - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(query) - ) { - while (rs.next()) { - result.add( - new NetworkAddress.Builder() - .stream(rs.getLong("stream")) - .ipv6(rs.getBytes("address")) - .port(rs.getInt("port")) - .services(rs.getLong("services")) - .time(rs.getLong("time")) - .build() - ); - } - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - if (result.isEmpty()) { - synchronized (this) { - if (stableNodes == null) { - stableNodes = loadStableNodes(); - } - } - for (long stream : streams) { - Set<NetworkAddress> nodes = stableNodes.get(stream); - if (nodes != null && !nodes.isEmpty()) { - result.add(Collections.selectRandom(nodes)); - } - } - if (result.isEmpty()) { - // There might have been an error resolving domain names due to a missing internet exception. - // Try to load the stable nodes again next time. - stableNodes = null; - } - } - return result; - } - - @Override - public void offerAddresses(List<NetworkAddress> nodes) { - cleanUp(); - nodes.stream() - .filter(node -> node.getTime() < now(+2 * MINUTE) && node.getTime() > now(-28 * DAY)) - .forEach(node -> { - synchronized (this) { - NetworkAddress existing = loadExisting(node); - if (existing == null) { - insert(node); - } else if (node.getTime() > existing.getTime()) { - update(node); - } - } - }); - } - - private void insert(NetworkAddress node) { - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Node (stream, address, port, services, time) " + - "VALUES (?, ?, ?, ?, ?)") - ) { - ps.setLong(1, node.getStream()); - ps.setBytes(2, node.getIPv6()); - ps.setInt(3, node.getPort()); - ps.setLong(4, node.getServices()); - ps.setLong(5, node.getTime()); - ps.executeUpdate(); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } - - private void update(NetworkAddress node) { - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement( - "UPDATE Node SET services=?, time=? WHERE stream=? AND address=? AND port=?") - ) { - ps.setLong(1, node.getServices()); - ps.setLong(2, node.getTime()); - ps.setLong(3, node.getStream()); - ps.setBytes(4, node.getIPv6()); - ps.setInt(5, node.getPort()); - ps.executeUpdate(); - } 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 deleted file mode 100644 index 0fda3fa..0000000 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java +++ /dev/null @@ -1,134 +0,0 @@ -package ch.dissem.bitmessage.repository; - -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.ProofOfWorkRepository; -import ch.dissem.bitmessage.utils.Strings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.sql.*; -import java.util.LinkedList; -import java.util.List; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * @author Christian Basler - */ -public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository, InternalContext.ContextHolder { - private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class); - private InternalContext ctx; - - 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, expiration_time, message_id FROM POW WHERE initial_hash=?") - ) { - ps.setBytes(1, initialHash); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - Blob data = rs.getBlob("data"); - if (rs.getObject("message_id") == null) { - return new Item( - Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), - rs.getLong("nonce_trials_per_byte"), - rs.getLong("extra_bytes") - ); - } else { - return new Item( - Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), - rs.getLong("nonce_trials_per_byte"), - rs.getLong("extra_bytes"), - rs.getLong("expiration_time"), - ctx.getMessageRepository().getMessage(rs.getLong("message_id")) - ); - } - } else { - throw new IllegalArgumentException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); - } - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - } - - @Override - public List<byte[]> getItems() { - try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT initial_hash FROM POW") - ) { - List<byte[]> result = new LinkedList<>(); - while (rs.next()) { - result.add(rs.getBytes("initial_hash")); - } - return result; - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - throw new ApplicationException(e); - } - } - - @Override - public void putObject(Item item) { - try ( - Connection connection = config.getConnection(); - PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, " + - "nonce_trials_per_byte, extra_bytes, expiration_time, message_id) " + - "VALUES (?, ?, ?, ?, ?, ?, ?)") - ) { - ps.setBytes(1, cryptography().getInitialHash(item.object)); - writeBlob(ps, 2, item.object); - ps.setLong(3, item.object.getVersion()); - ps.setLong(4, item.nonceTrialsPerByte); - ps.setLong(5, item.extraBytes); - - if (item.message == null) { - ps.setObject(6, null); - ps.setObject(7, null); - } else { - ps.setLong(6, item.expirationTime); - ps.setLong(7, (Long) item.message.getId()); - } - ps.executeUpdate(); - } catch (IOException | SQLException e) { - LOG.debug("Error storing object of type " + item.object.getPayload().getClass().getSimpleName(), e); - throw new ApplicationException(e); - } - } - - @Override - public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - putObject(new Item(object, nonceTrialsPerByte, extraBytes)); - } - - @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); - } - } - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } -} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcAddressRepository.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcAddressRepository.kt new file mode 100644 index 0000000..29a0f87 --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcAddressRepository.kt @@ -0,0 +1,203 @@ +/* + * 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.entity.BitmessageAddress +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.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.ports.AddressRepository +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.sql.PreparedStatement +import java.sql.SQLException +import java.util.* + +class JdbcAddressRepository(config: JdbcConfig) : JdbcHelper(config), AddressRepository { + + override fun findContact(ripeOrTag: ByteArray) = find("private_key is null").firstOrNull { + if (it.version > 3) { + Arrays.equals(ripeOrTag, it.tag) + } else { + Arrays.equals(ripeOrTag, it.ripe) + } + } + + override fun findIdentity(ripeOrTag: ByteArray) = find("private_key is not null").firstOrNull { + if (it.version > 3) { + Arrays.equals(ripeOrTag, it.tag) + } else { + Arrays.equals(ripeOrTag, it.ripe) + } + } + + override fun getIdentities() = find("private_key IS NOT NULL") + + override fun getChans() = find("chan = '1'") + + override fun getSubscriptions() = find("subscribed = '1'") + + override fun getSubscriptions(broadcastVersion: Long): List<BitmessageAddress> = if (broadcastVersion > 4) { + find("subscribed = '1' AND version > 3") + } else { + find("subscribed = '1' AND version <= 3") + } + + override fun getContacts() = find("private_key IS NULL OR chan = '1'") + + private fun find(where: String): List<BitmessageAddress> { + val result = LinkedList<BitmessageAddress>() + try { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery(""" + SELECT address, alias, public_key, private_key, subscribed, chan + FROM Address + WHERE $where + """).use { rs -> + while (rs.next()) { + val address: BitmessageAddress + + val privateKeyStream = rs.getBinaryStream("private_key") + if (privateKeyStream == null) { + address = BitmessageAddress(rs.getString("address")) + rs.getBlob("public_key")?.let { publicKeyBlob -> + var pubkey: Pubkey = Factory.readPubkey(address.version, address.stream, + publicKeyBlob.binaryStream, publicKeyBlob.length().toInt(), false)!! + if (address.version == 4L && pubkey is V3Pubkey) { + pubkey = V4Pubkey(pubkey) + } + address.pubkey = pubkey + } + } else { + val privateKey = PrivateKey.read(privateKeyStream) + address = BitmessageAddress(privateKey) + } + address.alias = rs.getString("alias") + address.isSubscribed = rs.getBoolean("subscribed") + address.isChan = rs.getBoolean("chan") + + result.add(address) + } + } + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + return result + } + + private fun exists(address: BitmessageAddress): Boolean { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery("SELECT '1' FROM Address " + + "WHERE address='" + address.address + "'").use { rs -> return rs.next() } + } + } + } + + override fun save(address: BitmessageAddress) { + try { + if (exists(address)) { + update(address) + } else { + insert(address) + } + } catch (e: IOException) { + LOG.error(e.message, e) + } catch (e: SQLException) { + LOG.error(e.message, e) + } + } + + private fun update(address: BitmessageAddress) { + val statement = StringBuilder("UPDATE Address SET alias=?") + if (address.pubkey != null) { + statement.append(", public_key=?") + } + if (address.privateKey != null) { + statement.append(", private_key=?") + } + statement.append(", subscribed=?, chan=? WHERE address=?") + config.getConnection().use { connection -> + connection.prepareStatement(statement.toString()).use { ps -> + var i = 0 + ps.setString(++i, address.alias) + if (address.pubkey != null) { + writePubkey(ps, ++i, address.pubkey) + } + if (address.privateKey != null) { + JdbcHelper.writeBlob(ps, ++i, address.privateKey) + } + ps.setBoolean(++i, address.isSubscribed) + ps.setBoolean(++i, address.isChan) + ps.setString(++i, address.address) + ps.executeUpdate() + } + } + } + + private fun insert(address: BitmessageAddress) { + config.getConnection().use { connection -> + connection.prepareStatement( + "INSERT INTO Address (address, version, alias, public_key, private_key, subscribed, chan) " + "VALUES (?, ?, ?, ?, ?, ?, ?)").use { ps -> + ps.setString(1, address.address) + ps.setLong(2, address.version) + ps.setString(3, address.alias) + writePubkey(ps, 4, address.pubkey) + JdbcHelper.writeBlob(ps, 5, address.privateKey) + ps.setBoolean(6, address.isSubscribed) + ps.setBoolean(7, address.isChan) + ps.executeUpdate() + } + } + } + + private fun writePubkey(ps: PreparedStatement, parameterIndex: Int, data: Pubkey?) { + if (data != null) { + val out = ByteArrayOutputStream() + data.writeUnencrypted(out) + ps.setBytes(parameterIndex, out.toByteArray()) + } else { + ps.setBytes(parameterIndex, null) + } + } + + override fun remove(address: BitmessageAddress) { + try { + config.getConnection().use { connection -> connection.createStatement().use { stmt -> stmt.executeUpdate("DELETE FROM Address WHERE address = '" + address.address + "'") } } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + } + + override fun getAddress(address: String): BitmessageAddress? { + val result = find("address = '$address'") + if (result.isNotEmpty()) return result[0] + return null + } + + companion object { + private val LOG = LoggerFactory.getLogger(JdbcAddressRepository::class.java) + } +} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcConfig.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcConfig.kt new file mode 100644 index 0000000..311bf70 --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcConfig.kt @@ -0,0 +1,39 @@ +/* + * 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 org.flywaydb.core.Flyway +import java.sql.DriverManager + +/** + * The base configuration for all JDBC based repositories. You should only make one instance, + * as flyway initializes/updates the database at object creation. + */ +open class JdbcConfig @JvmOverloads constructor( + protected val dbUrl: String = "jdbc:h2:~/jabit;AUTO_SERVER=TRUE", + protected val dbUser: String = "sa", + protected val dbPassword: String? = null +) { + protected val flyway = Flyway() + + init { + flyway.setDataSource(dbUrl, dbUser, dbPassword) + flyway.migrate() + } + + fun getConnection() = DriverManager.getConnection(dbUrl, dbUser, dbPassword) +} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcHelper.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcHelper.kt new file mode 100644 index 0000000..ce8ca1f --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcHelper.kt @@ -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.entity.Streamable +import java.io.ByteArrayOutputStream +import java.sql.PreparedStatement + +/** + * Helper class that does Flyway migration, provides JDBC connections and some helper methods. + */ +abstract class JdbcHelper protected constructor(protected val config: JdbcConfig) { + companion object { + @JvmStatic fun writeBlob(ps: PreparedStatement, parameterIndex: Int, data: Streamable?) { + if (data == null) { + ps.setBytes(parameterIndex, null) + } else { + val os = ByteArrayOutputStream() + data.write(os) + ps.setBytes(parameterIndex, os.toByteArray()) + } + } + } +} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcInventory.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcInventory.kt new file mode 100644 index 0000000..07d8efc --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcInventory.kt @@ -0,0 +1,166 @@ +/* + * 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.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.ObjectType +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.ports.Inventory +import ch.dissem.bitmessage.utils.SqlStrings.join +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import ch.dissem.bitmessage.utils.UnixTime.now +import org.slf4j.LoggerFactory +import java.sql.SQLException +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class JdbcInventory(config: JdbcConfig) : JdbcHelper(config), Inventory { + + private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>() + + override fun getInventory(vararg streams: Long): List<InventoryVector> { + val result = LinkedList<InventoryVector>() + for (stream in streams) { + getCache(stream).entries.stream() + .filter { e -> e.value > now } + .forEach { e -> result.add(e.key) } + } + return result + } + + private fun getCache(stream: Long): MutableMap<InventoryVector, Long> { + var result: MutableMap<InventoryVector, Long>? = cache[stream] + if (result == null) { + synchronized(cache) { + if (cache[stream] == null) { + val map = ConcurrentHashMap<InventoryVector, Long>() + cache.put(stream, map) + result = map + try { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery("SELECT hash, expires FROM Inventory " + + "WHERE expires > " + (now - 5 * MINUTE) + " AND stream = " + stream).use { rs -> + while (rs.next()) { + map.put(InventoryVector(rs.getBytes("hash")), rs.getLong("expires")) + } + } + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + } + } + } + return result!! + } + + override fun getMissing(offer: List<InventoryVector>, vararg streams: Long): List<InventoryVector> = offer - streams.flatMap { getCache(it).keys } + + override fun getObject(vector: InventoryVector): ObjectMessage? { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery("SELECT data, version FROM Inventory WHERE hash = X'$vector'").use { rs -> + if (rs.next()) { + val data = rs.getBlob("data") + return Factory.getObjectMessage(rs.getInt("version"), data.binaryStream, data.length().toInt()) + } else { + LOG.info("Object requested that we don't have. IV: " + vector) + return null + } + } + } + } + } + + override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> { + val query = StringBuilder("SELECT data, version FROM Inventory WHERE 1=1") + if (stream > 0) { + query.append(" AND stream = ").append(stream) + } + if (version > 0) { + query.append(" AND version = ").append(version) + } + if (types.isNotEmpty()) { + query.append(" AND type IN (").append(join(*types)).append(')') + } + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery(query.toString()).use { rs -> + val result = LinkedList<ObjectMessage>() + while (rs.next()) { + val data = rs.getBlob("data") + result.add(Factory.getObjectMessage(rs.getInt("version"), data.binaryStream, data.length().toInt())!!) + } + return result + } + } + } + } + + override fun storeObject(objectMessage: ObjectMessage) { + if (getCache(objectMessage.stream).containsKey(objectMessage.inventoryVector)) + return + + try { + config.getConnection().use { connection -> + connection.prepareStatement("INSERT INTO Inventory " + "(hash, stream, expires, data, type, version) VALUES (?, ?, ?, ?, ?, ?)").use { ps -> + val iv = objectMessage.inventoryVector + LOG.trace("Storing object " + iv) + ps.setBytes(1, iv.hash) + ps.setLong(2, objectMessage.stream) + ps.setLong(3, objectMessage.expiresTime) + JdbcHelper.Companion.writeBlob(ps, 4, objectMessage) + ps.setLong(5, objectMessage.type) + ps.setLong(6, objectMessage.version) + ps.executeUpdate() + getCache(objectMessage.stream).put(iv, objectMessage.expiresTime) + } + } + } catch (e: SQLException) { + LOG.debug("Error storing object of type " + objectMessage.payload.javaClass.simpleName, e) + } catch (e: Exception) { + LOG.error(e.message, e) + } + } + + override fun contains(objectMessage: ObjectMessage): Boolean { + return getCache(objectMessage.stream).any { (key, _) -> key == objectMessage.inventoryVector } + } + + override fun cleanup() { + try { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeUpdate("DELETE FROM Inventory WHERE expires < " + (now - 5 * MINUTE)) + } + } + } catch (e: SQLException) { + LOG.debug(e.message, e) + } + + for (c in cache.values) { + c.entries.removeAll { e -> e.value < now - 5 * MINUTE } + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(JdbcInventory::class.java) + } +} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt new file mode 100644 index 0000000..0717e8e --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt @@ -0,0 +1,343 @@ +/* + * 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.entity.Plaintext +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.ports.AbstractMessageRepository +import ch.dissem.bitmessage.ports.MessageRepository +import ch.dissem.bitmessage.repository.JdbcHelper.Companion.writeBlob +import org.slf4j.LoggerFactory +import java.io.IOException +import java.sql.Connection +import java.sql.ResultSet +import java.sql.SQLException +import java.sql.Statement +import java.util.* + +class JdbcMessageRepository(private val config: JdbcConfig) : AbstractMessageRepository(), MessageRepository { + + override fun findLabels(where: String): List<Label> { + try { + config.getConnection().use { + connection -> + return findLabels(connection, where) + } + } catch (e: SQLException) { + LOG.error(e.message, e) + return ArrayList() + } + } + + private fun getLabel(rs: ResultSet): Label { + val typeName = rs.getString("type") + val type = if (typeName == null) { + null + } else { + Label.Type.valueOf(typeName) + } + val label = Label(rs.getString("label"), type, rs.getInt("color")) + label.id = rs.getLong("id") + + return label + } + + override fun countUnread(label: Label?): Int { + val where = if (label == null) { + "" + } else { + "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) AND " + } + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + + "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name + "'))" + + try { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery("SELECT count(*) FROM Message WHERE $where").use { rs -> + if (rs.next()) { + return rs.getInt(1) + } + } + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + return 0 + } + + override fun find(where: String): List<Plaintext> { + val result = LinkedList<Plaintext>() + try { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery( + """SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try, conversation + FROM Message WHERE $where""").use { rs -> + while (rs.next()) { + val iv = rs.getBytes("iv") + val data = rs.getBinaryStream("data") + val type = Plaintext.Type.valueOf(rs.getString("type")) + val builder = Plaintext.readWithoutSignature(type, data) + val id = rs.getLong("id") + builder.id(id) + builder.IV(InventoryVector.fromHash(iv)) + builder.from(ctx.addressRepository.getAddress(rs.getString("sender"))!!) + rs.getString("recipient")?.let { builder.to(ctx.addressRepository.getAddress(it)) } + builder.ackData(rs.getBytes("ack_data")) + builder.sent(rs.getObject("sent") as Long?) + builder.received(rs.getObject("received") as Long?) + builder.status(Plaintext.Status.valueOf(rs.getString("status"))) + builder.ttl(rs.getLong("ttl")) + builder.retries(rs.getInt("retries")) + builder.nextTry(rs.getObject("next_try") as Long?) + builder.conversation(rs.getObject("conversation") as UUID? ?: UUID.randomUUID()) + builder.labels(findLabels(connection, + "id IN (SELECT label_id FROM Message_Label WHERE message_id=$id) ORDER BY ord")) + val message = builder.build() + message.initialHash = rs.getBytes("initial_hash") + result.add(message) + } + } + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + return result + } + + private fun findLabels(connection: Connection, where: String): List<Label> { + val result = ArrayList<Label>() + try { + connection.createStatement().use { stmt -> + stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE $where").use { rs -> + while (rs.next()) { + result.add(getLabel(rs)) + } + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + return result + } + + override fun save(message: Plaintext) { + saveContactIfNecessary(message.from) + saveContactIfNecessary(message.to) + + config.getConnection().use { connection -> + try { + connection.autoCommit = false + save(connection, message) + updateParents(connection, message) + updateLabels(connection, message) + connection.commit() + } catch (e: Exception) { + connection.rollback() + throw e + } + } + } + + private fun save(connection: Connection, message: Plaintext) { + if (message.id == null) { + insert(connection, message) + } else { + update(connection, message) + } + } + + private fun updateLabels(connection: Connection, message: Plaintext) { + // remove existing labels + connection.createStatement().use { stmt -> stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id=${message.id!!}") } + // save new labels + connection.prepareStatement("INSERT INTO Message_Label VALUES (${message.id}, ?)").use { ps -> + for (label in message.labels) { + ps.setLong(1, (label.id as Long?)!!) + ps.executeUpdate() + } + } + } + + private fun updateParents(connection: Connection, message: Plaintext) { + if (message.inventoryVector == null || message.parents.isEmpty()) { + // There are no parents to save yet (they are saved in the extended data, that's enough for now) + return + } + // remove existing parents + connection.prepareStatement("DELETE FROM Message_Parent WHERE child=?").use { ps -> + ps.setBytes(1, message.initialHash) + ps.executeUpdate() + } + val childIV = message.inventoryVector!!.hash + // save new parents + var order = 0 + connection.prepareStatement("INSERT INTO Message_Parent VALUES (?, ?, ?, ?)").use { ps -> + for (parentIV in message.parents) { + val parent = getMessage(parentIV) + mergeConversations(connection, parent!!.conversationId, message.conversationId) + order++ + ps.setBytes(1, parentIV.hash) + ps.setBytes(2, childIV) + ps.setInt(3, order) // FIXME: this might not be necessary + ps.setObject(4, message.conversationId) + ps.executeUpdate() + } + } + } + + private fun insert(connection: Connection, message: Plaintext) { + connection.prepareStatement( + "INSERT INTO Message (iv, type, sender, recipient, data, ack_data, sent, received, " + + "status, initial_hash, ttl, retries, next_try, conversation) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS).use { ps -> + ps.setBytes(1, if (message.inventoryVector == null) null else message.inventoryVector!!.hash) + ps.setString(2, message.type.name) + ps.setString(3, message.from.address) + ps.setString(4, if (message.to == null) null else message.to!!.address) + writeBlob(ps, 5, message) + ps.setBytes(6, message.ackData) + ps.setObject(7, message.sent) + ps.setObject(8, message.received) + ps.setString(9, message.status.name) + ps.setBytes(10, message.initialHash) + ps.setLong(11, message.ttl) + ps.setInt(12, message.retries) + ps.setObject(13, message.nextTry) + ps.setObject(14, message.conversationId) + + ps.executeUpdate() + // get generated id + ps.generatedKeys.use { rs -> + rs.next() + message.id = rs.getLong(1) + } + } + } + + private fun update(connection: Connection, message: Plaintext) { + connection.prepareStatement( + "UPDATE Message SET iv=?, type=?, sender=?, recipient=?, data=?, ack_data=?, sent=?, received=?, " + + "status=?, initial_hash=?, ttl=?, retries=?, next_try=? " + + "WHERE id=?").use { ps -> + ps.setBytes(1, if (message.inventoryVector == null) null else message.inventoryVector!!.hash) + ps.setString(2, message.type.name) + ps.setString(3, message.from.address) + ps.setString(4, if (message.to == null) null else message.to!!.address) + writeBlob(ps, 5, message) + ps.setBytes(6, message.ackData) + ps.setObject(7, message.sent) + ps.setObject(8, message.received) + ps.setString(9, message.status.name) + ps.setBytes(10, message.initialHash) + ps.setLong(11, message.ttl) + ps.setInt(12, message.retries) + ps.setObject(13, message.nextTry) + ps.setLong(14, (message.id as Long?)!!) + ps.executeUpdate() + } + } + + override fun remove(message: Plaintext) { + try { + config.getConnection().use { connection -> + connection.autoCommit = false + try { + connection.createStatement().use { stmt -> + stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id = " + message.id!!) + stmt.executeUpdate("DELETE FROM Message WHERE id = " + message.id!!) + connection.commit() + } + } catch (e: SQLException) { + try { + connection.rollback() + } catch (e1: SQLException) { + LOG.debug(e1.message, e) + } + + LOG.error(e.message, e) + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + } + + override fun findConversations(label: Label?): List<UUID> { + val where: String + if (label == null) { + where = "id NOT IN (SELECT message_id FROM Message_Label)" + } else { + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")" + } + val result = LinkedList<UUID>() + try { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery( + "SELECT DISTINCT conversation FROM Message WHERE " + where).use { rs -> + while (rs.next()) { + result.add(rs.getObject(1) as UUID) + } + } + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + return result + } + + /** + * Replaces every occurrence of the source conversation ID with the target ID + + * @param source ID of the conversation to be merged + * * + * @param target ID of the merge target + */ + private fun mergeConversations(connection: Connection, source: UUID, target: UUID) { + try { + connection.prepareStatement( + "UPDATE Message SET conversation=? WHERE conversation=?").use { ps1 -> + connection.prepareStatement( + "UPDATE Message_Parent SET conversation=? WHERE conversation=?").use { ps2 -> + ps1.setObject(1, target) + ps1.setObject(2, source) + ps1.executeUpdate() + ps2.setObject(1, target) + ps2.setObject(2, source) + ps2.executeUpdate() + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + + } + + companion object { + private val LOG = LoggerFactory.getLogger(JdbcMessageRepository::class.java) + } +} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcNodeRegistry.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcNodeRegistry.kt new file mode 100644 index 0000000..ccdbe75 --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcNodeRegistry.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.ports.NodeRegistry +import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes +import ch.dissem.bitmessage.utils.Collections +import ch.dissem.bitmessage.utils.SqlStrings +import ch.dissem.bitmessage.utils.Strings +import ch.dissem.bitmessage.utils.UnixTime.DAY +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import ch.dissem.bitmessage.utils.UnixTime.now +import org.slf4j.LoggerFactory +import java.sql.SQLException +import java.util.* + +class JdbcNodeRegistry(config: JdbcConfig) : JdbcHelper(config), NodeRegistry { + private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap() + get() { + if (field.isEmpty()) + field = loadStableNodes() + return field + } + + init { + cleanUp() + } + + private fun cleanUp() { + try { + config.getConnection().use { connection -> + connection.prepareStatement("DELETE FROM Node WHERE time<?").use { ps -> + ps.setLong(1, now - 28 * DAY) + ps.executeUpdate() + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + } + + private fun loadExisting(node: NetworkAddress): NetworkAddress? { + val query = """ + SELECT stream, address, port, services, time + FROM Node + WHERE stream = ${node.stream} AND address = X'${Strings.hex(node.IPv6)}' AND port = ${node.port} + """ + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery(query).use { rs -> + if (rs.next()) { + return NetworkAddress.Builder() + .stream(rs.getLong("stream")) + .ipv6(rs.getBytes("address")) + .port(rs.getInt("port")) + .services(rs.getLong("services")) + .time(rs.getLong("time")) + .build() + } else { + return null + } + } + } + } + } + + override fun clear() { + try { + config.getConnection().use { connection -> + connection.prepareStatement("DELETE FROM Node").use { ps -> ps.executeUpdate() } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + } + + override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> { + val result = LinkedList<NetworkAddress>() + val query = """ + SELECT stream, address, port, services, time + FROM Node + WHERE stream IN (${SqlStrings.join(*streams)}) + ORDER BY TIME DESC LIMIT $limit + """ + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery(query).use { rs -> + while (rs.next()) { + result.add( + NetworkAddress.Builder() + .stream(rs.getLong("stream")) + .ipv6(rs.getBytes("address")) + .port(rs.getInt("port")) + .services(rs.getLong("services")) + .time(rs.getLong("time")) + .build() + ) + } + } + } + } + + if (result.isEmpty()) { + streams + .asSequence() + .mapNotNull { stableNodes[it] } + .filter { it.isNotEmpty() } + .mapTo(result) { Collections.selectRandom(it) } + if (result.isEmpty()) { + // There might have been an error resolving domain names due to a missing internet connection. + // Try to load the stable nodes again next time. + stableNodes = emptyMap() + } + } + return result + } + + override fun offerAddresses(nodes: List<NetworkAddress>) { + cleanUp() + nodes.stream() + .filter { (time) -> time < now + 2 * MINUTE && time > now - 28 * DAY } + .forEach { node -> + synchronized(this) { + val existing = loadExisting(node) + if (existing == null) { + insert(node) + } else if (node.time > existing.time) { + update(node) + } + } + } + } + + private fun insert(node: NetworkAddress) { + try { + config.getConnection().use { connection -> + connection.prepareStatement( + "INSERT INTO Node (stream, address, port, services, time) VALUES (?, ?, ?, ?, ?)").use { ps -> + ps.setLong(1, node.stream) + ps.setBytes(2, node.IPv6) + ps.setInt(3, node.port) + ps.setLong(4, node.services) + ps.setLong(5, node.time) + ps.executeUpdate() + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + } + + private fun update(node: NetworkAddress) { + try { + config.getConnection().use { connection -> + connection.prepareStatement( + "UPDATE Node SET services=?, time=? WHERE stream=? AND address=? AND port=?").use { ps -> + ps.setLong(1, node.services) + ps.setLong(2, node.time) + ps.setLong(3, node.stream) + ps.setBytes(4, node.IPv6) + ps.setInt(5, node.port) + ps.executeUpdate() + } + } + } catch (e: SQLException) { + LOG.error(e.message, e) + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(JdbcNodeRegistry::class.java) + } +} diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.kt new file mode 100644 index 0000000..96567de --- /dev/null +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.ports.ProofOfWorkRepository +import ch.dissem.bitmessage.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.Strings +import org.slf4j.LoggerFactory +import java.sql.SQLException + +/** + * @author Christian Basler + */ +class JdbcProofOfWorkRepository(config: JdbcConfig) : JdbcHelper(config), ProofOfWorkRepository, InternalContext.ContextHolder { + private lateinit var ctx: InternalContext + + override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item { + config.getConnection().use { connection -> + connection.prepareStatement(""" + SELECT data, version, nonce_trials_per_byte, extra_bytes, expiration_time, message_id + FROM POW + WHERE initial_hash=? + """).use { ps -> + ps.setBytes(1, initialHash) + ps.executeQuery().use { rs -> + if (rs.next()) { + val data = rs.getBlob("data") + if (rs.getObject("message_id") == null) { + return ProofOfWorkRepository.Item( + Factory.getObjectMessage(rs.getInt("version"), data.binaryStream, data.length().toInt())!!, + rs.getLong("nonce_trials_per_byte"), + rs.getLong("extra_bytes") + ) + } else { + return ProofOfWorkRepository.Item( + Factory.getObjectMessage(rs.getInt("version"), data.binaryStream, data.length().toInt())!!, + rs.getLong("nonce_trials_per_byte"), + rs.getLong("extra_bytes"), + rs.getLong("expiration_time"), + ctx.messageRepository.getMessage(rs.getLong("message_id")) + ) + } + } else { + throw IllegalArgumentException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)) + } + } + } + } + } + + override fun getItems(): List<ByteArray> { + config.getConnection().use { connection -> + connection.createStatement().use { stmt -> + stmt.executeQuery("SELECT initial_hash FROM POW").use { rs -> + val result = mutableListOf<ByteArray>() + while (rs.next()) { + result.add(rs.getBytes("initial_hash")) + } + return result + } + } + } + } + + override fun putObject(item: ProofOfWorkRepository.Item) { + config.getConnection().use { connection -> + connection.prepareStatement(""" + INSERT INTO + POW (initial_hash, data, version, nonce_trials_per_byte, extra_bytes, expiration_time, message_id) + VALUES (?, ?, ?, ?, ?, ?, ?)""").use { ps -> + ps.setBytes(1, cryptography().getInitialHash(item.objectMessage)) + JdbcHelper.Companion.writeBlob(ps, 2, item.objectMessage) + ps.setLong(3, item.objectMessage.version) + ps.setLong(4, item.nonceTrialsPerByte) + ps.setLong(5, item.extraBytes) + + if (item.message == null) { + ps.setObject(6, null) + ps.setObject(7, null) + } else { + ps.setLong(6, item.expirationTime!!) + ps.setLong(7, item.message!!.id as Long) + } + ps.executeUpdate() + } + } + } + + override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) { + putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes)) + } + + override fun removeObject(initialHash: ByteArray) { + try { + config.getConnection().use { connection -> + connection.prepareStatement("DELETE FROM POW WHERE initial_hash=?").use { ps -> + ps.setBytes(1, initialHash) + ps.executeUpdate() + } + } + } catch (e: SQLException) { + LOG.debug(e.message, e) + } + } + + override fun setContext(context: InternalContext) { + ctx = context + } + + companion object { + private val LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository::class.java) + } +} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java deleted file mode 100644 index 7f3098b..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java +++ /dev/null @@ -1,184 +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.repository; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import org.junit.Before; -import org.junit.Test; - -import java.util.List; - -import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; -import static org.junit.Assert.*; - -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"; - public String IDENTITY_A; - public String IDENTITY_B; - - private TestJdbcConfig config; - private JdbcAddressRepository repo; - - @Before - public void setUp() throws InterruptedException { - config = new TestJdbcConfig(); - config.reset(); - - repo = new JdbcAddressRepository(config); - - repo.save(new BitmessageAddress(CONTACT_A)); - repo.save(new BitmessageAddress(CONTACT_B)); - repo.save(new BitmessageAddress(CONTACT_C)); - - BitmessageAddress identityA = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); - repo.save(identityA); - IDENTITY_A = identityA.getAddress(); - BitmessageAddress identityB = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); - repo.save(identityB); - IDENTITY_B = identityB.getAddress(); - } - - @Test - public void testFindContact() throws Exception { - BitmessageAddress address = new BitmessageAddress(CONTACT_A); - assertEquals(4, address.getVersion()); - assertEquals(address, repo.findContact(address.getTag())); - assertNull(repo.findIdentity(address.getTag())); - } - - @Test - public void testFindIdentity() throws Exception { - BitmessageAddress identity = new BitmessageAddress(IDENTITY_A); - assertEquals(4, identity.getVersion()); - assertNull(repo.findContact(identity.getTag())); - - BitmessageAddress storedIdentity = repo.findIdentity(identity.getTag()); - assertEquals(identity, storedIdentity); - assertTrue(storedIdentity.has(Pubkey.Feature.DOES_ACK)); - } - - @Test - public void testGetIdentities() throws Exception { - List<BitmessageAddress> identities = repo.getIdentities(); - assertEquals(2, identities.size()); - for (BitmessageAddress identity : identities) { - assertNotNull(identity.getPrivateKey()); - } - } - - @Test - public void testGetSubscriptions() throws Exception { - addSubscription("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - addSubscription("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh"); - List<BitmessageAddress> subscriptions = repo.getSubscriptions(); - assertEquals(3, subscriptions.size()); - } - - @Test - public void testGetSubscriptionsForVersion() throws Exception { - addSubscription("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - addSubscription("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh"); - - List<BitmessageAddress> subscriptions; - - subscriptions = repo.getSubscriptions(5); - assertEquals(1, subscriptions.size()); - - subscriptions = repo.getSubscriptions(4); - assertEquals(2, subscriptions.size()); - } - - @Test - public void testGetContacts() throws Exception { - List<BitmessageAddress> contacts = repo.getContacts(); - assertEquals(3, contacts.size()); - for (BitmessageAddress contact : contacts) { - assertNull(contact.getPrivateKey()); - } - } - - @Test - public void ensureNewAddressIsSaved() throws Exception { - repo.save(new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000))); - List<BitmessageAddress> identities = repo.getIdentities(); - assertEquals(3, identities.size()); - } - - @Test - public void ensureExistingAddressIsUpdated() throws Exception { - BitmessageAddress address = repo.getAddress(CONTACT_A); - address.setAlias("Test-Alias"); - repo.save(address); - address = repo.getAddress(address.getAddress()); - assertEquals("Test-Alias", address.getAlias()); - } - - @Test - public void ensureExistingKeysAreNotDeleted() { - BitmessageAddress address = new BitmessageAddress(IDENTITY_A); - address.setAlias("Test"); - repo.save(address); - BitmessageAddress identityA = repo.getAddress(IDENTITY_A); - assertNotNull(identityA.getPubkey()); - assertNotNull(identityA.getPrivateKey()); - assertEquals("Test", identityA.getAlias()); - assertFalse(identityA.isChan()); - } - - @Test - public void ensureNewChanIsSavedAndUpdated() { - BitmessageAddress chan = BitmessageAddress.chan(1, "test"); - repo.save(chan); - BitmessageAddress address = repo.getAddress(chan.getAddress()); - assertNotNull(address); - assertTrue(address.isChan()); - - address.setAlias("Test"); - repo.save(address); - - address = repo.getAddress(chan.getAddress()); - assertNotNull(address); - assertTrue(address.isChan()); - assertEquals("Test", address.getAlias()); - } - - @Test - public void testRemove() throws Exception { - BitmessageAddress address = repo.getAddress(IDENTITY_A); - repo.remove(address); - assertNull(repo.getAddress(IDENTITY_A)); - } - - @Test - public void testGetAddress() throws Exception { - BitmessageAddress address = repo.getAddress(IDENTITY_A); - assertNotNull(address); - assertNotNull(address.getPrivateKey()); - } - - private void addSubscription(String address) { - BitmessageAddress subscription = new BitmessageAddress(address); - subscription.setSubscribed(true); - repo.save(subscription); - } -} \ No newline at end of file diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java deleted file mode 100644 index 6f31299..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcInventoryTest.java +++ /dev/null @@ -1,143 +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.repository; - -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.GetPubkey; -import ch.dissem.bitmessage.entity.payload.ObjectPayload; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.ports.Inventory; -import org.junit.Before; -import org.junit.Test; - -import java.util.LinkedList; -import java.util.List; - -import static ch.dissem.bitmessage.entity.payload.ObjectType.GET_PUBKEY; -import static ch.dissem.bitmessage.entity.payload.ObjectType.MSG; -import static ch.dissem.bitmessage.utils.UnixTime.DAY; -import static ch.dissem.bitmessage.utils.UnixTime.now; -import static org.junit.Assert.*; - -public class JdbcInventoryTest extends TestBase { - private TestJdbcConfig config; - private Inventory inventory; - - private InventoryVector inventoryVector1; - private InventoryVector inventoryVector2; - private InventoryVector inventoryVectorIgnore; - - @Before - public void setUp() throws Exception { - config = new TestJdbcConfig(); - config.reset(); - - inventory = new JdbcInventory(config); - - ObjectMessage object1 = getObjectMessage(1, 300, getGetPubkey()); - inventoryVector1 = object1.getInventoryVector(); - inventory.storeObject(object1); - - ObjectMessage object2 = getObjectMessage(2, 300, getGetPubkey()); - inventoryVector2 = object2.getInventoryVector(); - inventory.storeObject(object2); - - ObjectMessage ignore = getObjectMessage(1, -1 * DAY, getGetPubkey()); - inventoryVectorIgnore = ignore.getInventoryVector(); - inventory.storeObject(ignore); - } - - @Test - public void testGetInventory() throws Exception { - List<InventoryVector> inventoryVectors = inventory.getInventory(1); - assertEquals(1, inventoryVectors.size()); - - inventoryVectors = inventory.getInventory(2); - assertEquals(1, inventoryVectors.size()); - } - - @Test - public void testGetMissing() throws Exception { - InventoryVector newIV = getObjectMessage(1, 200, getGetPubkey()).getInventoryVector(); - List<InventoryVector> offer = new LinkedList<>(); - offer.add(newIV); - offer.add(inventoryVector1); - List<InventoryVector> missing = inventory.getMissing(offer, 1, 2); - assertEquals(1, missing.size()); - assertEquals(newIV, missing.get(0)); - } - - @Test - public void testGetObject() throws Exception { - ObjectMessage object = inventory.getObject(inventoryVectorIgnore); - assertNotNull(object); - assertEquals(1, object.getStream()); - assertEquals(inventoryVectorIgnore, object.getInventoryVector()); - } - - @Test - public void testGetObjects() throws Exception { - List<ObjectMessage> objects = inventory.getObjects(1, 4); - assertEquals(2, objects.size()); - - objects = inventory.getObjects(1, 4, GET_PUBKEY); - assertEquals(2, objects.size()); - - objects = inventory.getObjects(1, 4, MSG); - assertEquals(0, objects.size()); - } - - @Test - public void testStoreObject() throws Exception { - ObjectMessage object = getObjectMessage(5, 0, getGetPubkey()); - inventory.storeObject(object); - - 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)); - inventory.cleanup(); - assertNull(inventory.getObject(inventoryVectorIgnore)); - } - - private ObjectMessage getObjectMessage(long stream, long TTL, ObjectPayload payload) { - return new ObjectMessage.Builder() - .nonce(new byte[8]) - .expiresTime(now(+TTL)) - .stream(stream) - .payload(payload) - .build(); - } - - private GetPubkey getGetPubkey() { - return new GetPubkey(new BitmessageAddress("BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt")); - } -} \ No newline at end of file diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java deleted file mode 100644 index 43be79f..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ /dev/null @@ -1,343 +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.repository; - -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.entity.valueobject.PrivateKey; -import ch.dissem.bitmessage.entity.valueobject.extended.Message; -import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.utils.TestUtils; -import ch.dissem.bitmessage.utils.UnixTime; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.junit.Before; -import org.junit.Test; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -public class JdbcMessageRepositoryTest extends TestBase { - private BitmessageAddress contactA; - private BitmessageAddress contactB; - private BitmessageAddress identity; - - private MessageRepository repo; - - private Label inbox; - private Label sent; - private Label drafts; - private Label unread; - - @Before - public void setUp() throws Exception { - TestJdbcConfig config = new TestJdbcConfig(); - config.reset(); - AddressRepository addressRepo = new JdbcAddressRepository(config); - repo = new JdbcMessageRepository(config); - new InternalContext(new BitmessageContext.Builder() - .cryptography(cryptography()) - .addressRepo(addressRepo) - .messageRepo(repo) - ); - - BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); - contactA = new BitmessageAddress(tmp.getAddress()); - contactA.setPubkey(tmp.getPubkey()); - addressRepo.save(contactA); - contactB = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj"); - addressRepo.save(contactB); - - identity = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); - addressRepo.save(identity); - - inbox = repo.getLabels(Label.Type.INBOX).get(0); - sent = repo.getLabels(Label.Type.SENT).get(0); - drafts = repo.getLabels(Label.Type.DRAFT).get(0); - unread = repo.getLabels(Label.Type.UNREAD).get(0); - - addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox, unread); - addMessage(identity, contactA, Plaintext.Status.DRAFT, drafts); - addMessage(identity, contactB, Plaintext.Status.DRAFT, unread); - } - - @Test - public void ensureLabelsAreRetrieved() throws Exception { - List<Label> labels = repo.getLabels(); - assertEquals(5, labels.size()); - } - - @Test - public void ensureLabelsCanBeRetrievedByType() throws Exception { - List<Label> labels = repo.getLabels(Label.Type.INBOX); - assertEquals(1, labels.size()); - assertEquals("Inbox", labels.get(0).toString()); - } - - @Test - public void ensureMessagesCanBeFoundByLabel() throws Exception { - List<Plaintext> messages = repo.findMessages(inbox); - assertEquals(1, messages.size()); - Plaintext m = messages.get(0); - assertEquals(contactA, m.getFrom()); - assertEquals(identity, m.getTo()); - assertEquals(Plaintext.Status.RECEIVED, m.getStatus()); - } - - @Test - public void ensureUnreadMessagesCanBeFoundForAllLabels() { - int unread = repo.countUnread(null); - assertThat(unread, is(2)); - } - - @Test - public void ensureUnreadMessagesCanBeFoundByLabel() { - int unread = repo.countUnread(inbox); - assertThat(unread, is(1)); - } - - @Test - public void ensureMessageCanBeRetrievedByInitialHash() { - byte[] initialHash = new byte[64]; - Plaintext message = repo.findMessages(contactA).get(0); - message.setInitialHash(initialHash); - repo.save(message); - Plaintext other = repo.getMessage(initialHash); - assertThat(other, is(message)); - } - - @Test - public void ensureAckMessageCanBeUpdatedAndRetrieved() { - byte[] initialHash = new byte[64]; - Plaintext message = repo.findMessages(contactA).get(0); - message.setInitialHash(initialHash); - ObjectMessage ackMessage = message.getAckMessage(); - repo.save(message); - Plaintext other = repo.getMessage(initialHash); - assertThat(other, is(message)); - assertThat(other.getAckMessage(), is(ackMessage)); - } - - @Test - public void testFindMessagesByStatus() throws Exception { - List<Plaintext> messages = repo.findMessages(Plaintext.Status.RECEIVED); - assertEquals(1, messages.size()); - Plaintext m = messages.get(0); - assertEquals(contactA, m.getFrom()); - assertEquals(identity, m.getTo()); - assertEquals(Plaintext.Status.RECEIVED, m.getStatus()); - } - - @Test - public void testFindMessagesByStatusAndRecipient() throws Exception { - List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT, contactB); - assertEquals(1, messages.size()); - Plaintext m = messages.get(0); - assertEquals(identity, m.getFrom()); - assertEquals(contactB, m.getTo()); - assertEquals(Plaintext.Status.DRAFT, m.getStatus()); - } - - @Test - public void testSave() throws Exception { - Plaintext message = new Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(identity) - .to(contactA) - .message("Subject", "Message") - .status(Plaintext.Status.DOING_PROOF_OF_WORK) - .build(); - repo.save(message); - - assertNotNull(message.getId()); - - message.addLabels(inbox); - repo.save(message); - - List<Plaintext> messages = repo.findMessages(Plaintext.Status.DOING_PROOF_OF_WORK); - - assertEquals(1, messages.size()); - assertNotNull(messages.get(0).getInventoryVector()); - } - - @Test - public void testUpdate() throws Exception { - List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); - Plaintext message = messages.get(0); - message.setInventoryVector(TestUtils.randomInventoryVector()); - repo.save(message); - - messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); - assertEquals(1, messages.size()); - assertNotNull(messages.get(0).getInventoryVector()); - } - - @Test - public void ensureMessageIsRemoved() throws Exception { - Plaintext toRemove = repo.findMessages(Plaintext.Status.DRAFT, contactB).get(0); - List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT); - assertEquals(2, messages.size()); - repo.remove(toRemove); - messages = repo.findMessages(Plaintext.Status.DRAFT); - assertThat(messages, hasSize(1)); - } - - @Test - public void ensureUnacknowledgedMessagesAreFoundForResend() throws Exception { - Plaintext message = new Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(identity) - .to(contactA) - .message("Subject", "Message") - .sent(UnixTime.now()) - .status(Plaintext.Status.SENT) - .ttl(2) - .build(); - message.updateNextTry(); - assertThat(message.getRetries(), is(1)); - assertThat(message.getNextTry(), greaterThan(UnixTime.now())); - assertThat(message.getNextTry(), lessThanOrEqualTo(UnixTime.now(+2))); - repo.save(message); - Thread.sleep(4100); // somewhat longer than 2*TTL - List<Plaintext> messagesToResend = repo.findMessagesToResend(); - assertThat(messagesToResend, hasSize(1)); - - message.updateNextTry(); - assertThat(message.getRetries(), is(2)); - assertThat(message.getNextTry(), greaterThan(UnixTime.now())); - repo.save(message); - messagesToResend = repo.findMessagesToResend(); - assertThat(messagesToResend, empty()); - } - - @Test - public void ensureParentsAreSaved() { - Plaintext parent = storeConversation(); - - List<Plaintext> responses = repo.findResponses(parent); - assertThat(responses, hasSize(2)); - assertThat(responses, hasItem(hasMessage("Re: new test", "Nice!"))); - assertThat(responses, hasItem(hasMessage("Re: new test", "PS: it did work!"))); - } - - @Test - public void ensureConversationCanBeRetrieved() { - Plaintext root = storeConversation(); - List<UUID> conversations = repo.findConversations(inbox); - assertThat(conversations, hasSize(2)); - assertThat(conversations, hasItem(root.getConversationId())); - } - - private Plaintext addMessage(BitmessageAddress from, BitmessageAddress to, Plaintext.Status status, Label... labels) { - ExtendedEncoding content = new Message.Builder() - .subject("Subject") - .body("Message") - .build(); - return addMessage(from, to, content, status, labels); - } - - private Plaintext addMessage(BitmessageAddress from, BitmessageAddress to, - ExtendedEncoding content, Plaintext.Status status, Label... labels) { - Plaintext message = new Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(from) - .to(to) - .message(content) - .status(status) - .labels(Arrays.asList(labels)) - .build(); - repo.save(message); - return message; - } - - private Plaintext storeConversation() { - Plaintext older = addMessage(identity, contactA, - new Message.Builder() - .subject("hey there") - .body("does it work?") - .build(), - Plaintext.Status.SENT, sent); - - Plaintext root = addMessage(identity, contactA, - new Message.Builder() - .subject("new test") - .body("There's a new test in town!") - .build(), - Plaintext.Status.SENT, sent); - - addMessage(contactA, identity, - new Message.Builder() - .subject("Re: new test") - .body("Nice!") - .addParent(root) - .build(), - Plaintext.Status.RECEIVED, inbox); - - addMessage(contactA, identity, - new Message.Builder() - .subject("Re: new test") - .body("PS: it did work!") - .addParent(root) - .addParent(older) - .build(), - Plaintext.Status.RECEIVED, inbox); - - return repo.getMessage(root.getId()); - } - - private Matcher<Plaintext> hasMessage(String subject, String body) { - return new BaseMatcher<Plaintext>() { - @Override - public void describeTo(Description description) { - description.appendText("Subject: ").appendText(subject); - description.appendText(", "); - description.appendText("Body: ").appendText(body); - } - - @Override - public boolean matches(Object item) { - if (item instanceof Plaintext) { - Plaintext message = (Plaintext) item; - if (subject != null && !subject.equals(message.getSubject())) { - return false; - } - if (body != null && !body.equals(message.getText())) { - return false; - } - return true; - } else { - return false; - } - } - }; - } -} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java deleted file mode 100644 index 48ae664..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java +++ /dev/null @@ -1,105 +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.repository; - -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.ports.NodeRegistry; -import ch.dissem.bitmessage.utils.UnixTime; -import org.junit.Before; -import org.junit.Test; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static ch.dissem.bitmessage.utils.UnixTime.now; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - -/** - * Please note that some tests fail if there is no internet connection, - * as the initial nodes' IP addresses are determined by DNS lookup. - */ -public class JdbcNodeRegistryTest extends TestBase { - private TestJdbcConfig config; - private NodeRegistry registry; - - @Before - public void setUp() throws Exception { - config = new TestJdbcConfig(); - config.reset(); - registry = new JdbcNodeRegistry(config); - - registry.offerAddresses(Arrays.asList( - createAddress(1, 8444, 1, now()), - createAddress(2, 8444, 1, now()), - createAddress(3, 8444, 1, now()), - createAddress(4, 8444, 2, now()) - )); - } - - @Test - public void ensureGetKnownNodesWithoutStreamsYieldsEmpty() { - assertThat(registry.getKnownAddresses(10), empty()); - } - - @Test - public void ensurePredefinedNodeIsReturnedWhenDatabaseIsEmpty() throws Exception { - config.reset(); - List<NetworkAddress> knownAddresses = registry.getKnownAddresses(2, 1); - assertEquals(1, knownAddresses.size()); - } - - @Test - public void testGetKnownAddresses() throws Exception { - List<NetworkAddress> knownAddresses = registry.getKnownAddresses(2, 1); - assertEquals(2, knownAddresses.size()); - - knownAddresses = registry.getKnownAddresses(1000, 1); - assertEquals(3, knownAddresses.size()); - } - - @Test - public void testOfferAddresses() throws Exception { - registry.offerAddresses(Arrays.asList( - createAddress(1, 8444, 1, now()), - createAddress(10, 8444, 1, now()), - createAddress(11, 8444, 1, now()) - )); - - List<NetworkAddress> knownAddresses = registry.getKnownAddresses(1000, 1); - assertEquals(5, knownAddresses.size()); - - registry.offerAddresses(Collections.singletonList( - createAddress(1, 8445, 1, now()) - )); - - knownAddresses = registry.getKnownAddresses(1000, 1); - assertEquals(6, knownAddresses.size()); - } - - private NetworkAddress createAddress(int lastByte, int port, long stream, long time) { - return new NetworkAddress.Builder() - .ipv6(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, lastByte) - .port(port) - .stream(stream) - .time(time) - .build(); - } -} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java deleted file mode 100644 index 4396eb5..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.bitmessage.repository; - -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.GenericPayload; -import ch.dissem.bitmessage.entity.payload.GetPubkey; -import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item; -import ch.dissem.bitmessage.utils.TestUtils; -import ch.dissem.bitmessage.utils.UnixTime; -import org.junit.Before; -import org.junit.Test; - -import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -/** - * @author Christian Basler - */ -public class JdbcProofOfWorkRepositoryTest extends TestBase { - private TestJdbcConfig config; - private JdbcProofOfWorkRepository repo; - private AddressRepository addressRepo; - private MessageRepository messageRepo; - - private byte[] initialHash1; - private byte[] initialHash2; - - @Before - public void setUp() throws Exception { - config = new TestJdbcConfig(); - config.reset(); - - addressRepo = new JdbcAddressRepository(config); - messageRepo = new JdbcMessageRepository(config); - repo = new JdbcProofOfWorkRepository(config); - InternalContext ctx = new InternalContext(new BitmessageContext.Builder() - .addressRepo(addressRepo) - .messageRepo(messageRepo) - .powRepo(repo) - .cryptography(cryptography()) - ); - - repo.putObject(new ObjectMessage.Builder() - .payload(new GetPubkey(new BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(), - 1000, 1000); - initialHash1 = repo.getItems().get(0); - - BitmessageAddress sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - BitmessageAddress recipient = TestUtils.loadContact(); - addressRepo.save(sender); - addressRepo.save(recipient); - Plaintext plaintext = new Plaintext.Builder(MSG) - .ackData(cryptography().randomBytes(32)) - .from(sender) - .to(recipient) - .message("Subject", "Message") - .status(Plaintext.Status.DOING_PROOF_OF_WORK) - .build(); - messageRepo.save(plaintext); - initialHash2 = cryptography().getInitialHash(plaintext.getAckMessage()); - repo.putObject(new Item( - plaintext.getAckMessage(), - 1000, 1000, - UnixTime.now(+10 * MINUTE), - plaintext - )); - } - - @Test - public void ensureObjectIsStored() throws Exception { - int sizeBefore = repo.getItems().size(); - repo.putObject(new ObjectMessage.Builder() - .payload(new GetPubkey(new BitmessageAddress("BM-2D9U2hv3YBMHM1zERP32anKfVKohyPN9x2"))).build(), - 1000, 1000); - assertThat(repo.getItems().size(), is(sizeBefore + 1)); - } - - @Test - public void ensureAckObjectsAreStored() throws Exception { - int sizeBefore = repo.getItems().size(); - BitmessageAddress sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); - BitmessageAddress recipient = TestUtils.loadContact(); - addressRepo.save(sender); - addressRepo.save(recipient); - Plaintext plaintext = new Plaintext.Builder(MSG) - .ackData(cryptography().randomBytes(32)) - .from(sender) - .to(recipient) - .message("Subject", "Message") - .status(Plaintext.Status.DOING_PROOF_OF_WORK) - .build(); - messageRepo.save(plaintext); - repo.putObject(new Item( - plaintext.getAckMessage(), - 1000, 1000, - UnixTime.now(+10 * MINUTE), - plaintext - )); - assertThat(repo.getItems().size(), is(sizeBefore + 1)); - } - - @Test - public void ensureItemCanBeRetrieved() { - Item item = repo.getItem(initialHash1); - assertThat(item, notNullValue()); - assertThat(item.object.getPayload(), instanceOf(GetPubkey.class)); - assertThat(item.nonceTrialsPerByte, is(1000L)); - assertThat(item.extraBytes, is(1000L)); - } - - @Test - public void ensureAckItemCanBeRetrieved() { - Item item = repo.getItem(initialHash2); - assertThat(item, notNullValue()); - assertThat(item.object.getPayload(), instanceOf(GenericPayload.class)); - assertThat(item.nonceTrialsPerByte, is(1000L)); - assertThat(item.extraBytes, is(1000L)); - assertThat(item.expirationTime, not(0)); - assertThat(item.message, notNullValue()); - assertThat(item.message.getFrom().getPrivateKey(), notNullValue()); - assertThat(item.message.getTo().getPubkey(), notNullValue()); - } - - @Test(expected = RuntimeException.class) - public void ensureRetrievingNonexistingItemThrowsException() { - repo.getItem(new byte[0]); - } - - @Test - public void ensureItemCanBeDeleted() { - repo.removeObject(initialHash1); - repo.removeObject(initialHash2); - assertTrue(repo.getItems().isEmpty()); - } - - @Test - public void ensureDeletionOfNonexistingItemIsHandledSilently() { - repo.removeObject(new byte[0]); - } -} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java deleted file mode 100644 index be386cd..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestBase.java +++ /dev/null @@ -1,38 +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.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 deleted file mode 100644 index edb8f85..0000000 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.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.repository; - -import org.h2.tools.Server; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.sql.SQLException; - -/** - * 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 { - private static final Logger LOG = LoggerFactory.getLogger(TestJdbcConfig.class); - - static { - try { - Server.createTcpServer().start(); - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - } - - public TestJdbcConfig() { - super("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", null); - } - - public void reset() { - flyway.clean(); - flyway.migrate(); - } -} diff --git a/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.kt b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.kt new file mode 100644 index 0000000..5e05aac --- /dev/null +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.kt @@ -0,0 +1,180 @@ +/* + * 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.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class JdbcAddressRepositoryTest : TestBase() { + private val CONTACT_A = "BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt" + private val CONTACT_B = "BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj" + private val CONTACT_C = "BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke" + + private lateinit var IDENTITY_A: String + private lateinit var IDENTITY_B: String + + private lateinit var config: TestJdbcConfig + private lateinit var repo: JdbcAddressRepository + + @Before + fun setUp() { + config = TestJdbcConfig() + config.reset() + + repo = JdbcAddressRepository(config) + + repo.save(BitmessageAddress(CONTACT_A)) + repo.save(BitmessageAddress(CONTACT_B)) + repo.save(BitmessageAddress(CONTACT_C)) + + val identityA = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + repo.save(identityA) + IDENTITY_A = identityA.address + val identityB = BitmessageAddress(PrivateKey(false, 1, 1000, 1000)) + repo.save(identityB) + IDENTITY_B = identityB.address + } + + @Test + fun `ensure contact can be found`() { + val address = BitmessageAddress(CONTACT_A) + assertEquals(4, address.version) + assertEquals(address, repo.findContact(address.tag!!)) + assertNull(repo.findIdentity(address.tag!!)) + } + + @Test + fun `ensure identity can be found`() { + val identity = BitmessageAddress(IDENTITY_A) + assertEquals(4, identity.version) + assertNull(repo.findContact(identity.tag!!)) + + val storedIdentity = repo.findIdentity(identity.tag!!) + assertEquals(identity, storedIdentity) + assertTrue(storedIdentity!!.has(DOES_ACK)) + } + + @Test + fun `ensure identities are retrieved`() { + val identities = repo.getIdentities() + assertEquals(2, identities.size.toLong()) + for (identity in identities) { + assertNotNull(identity.privateKey) + } + } + + @Test + fun `ensure subscriptions are retrieved`() { + addSubscription("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + addSubscription("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh") + val subscriptions = repo.getSubscriptions() + assertEquals(3, subscriptions.size.toLong()) + } + + @Test + fun `ensure subscriptions are retrieved for given version`() { + addSubscription("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + addSubscription("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh") + + var subscriptions = repo.getSubscriptions(5) + + assertEquals(1, subscriptions.size.toLong()) + + subscriptions = repo.getSubscriptions(4) + assertEquals(2, subscriptions.size.toLong()) + } + + @Test + fun `ensure contacts are retrieved`() { + val contacts = repo.getContacts() + assertEquals(3, contacts.size.toLong()) + for (contact in contacts) { + assertNull(contact.privateKey) + } + } + + @Test + fun `ensure new address is saved`() { + repo.save(BitmessageAddress(PrivateKey(false, 1, 1000, 1000))) + val identities = repo.getIdentities() + assertEquals(3, identities.size.toLong()) + } + + @Test + fun `ensure existing address is updated`() { + var address = repo.getAddress(CONTACT_A) + address!!.alias = "Test-Alias" + repo.save(address) + address = repo.getAddress(address.address) + assertEquals("Test-Alias", address!!.alias) + } + + @Test + fun `ensure existing keys are not deleted`() { + val address = BitmessageAddress(IDENTITY_A) + address.alias = "Test" + repo.save(address) + val identityA = repo.getAddress(IDENTITY_A) + assertNotNull(identityA!!.pubkey) + assertNotNull(identityA.privateKey) + assertEquals("Test", identityA.alias) + assertFalse(identityA.isChan) + } + + @Test + fun `ensure new chan is saved and updated`() { + val chan = BitmessageAddress.chan(1, "test") + repo.save(chan) + var address = repo.getAddress(chan.address) + assertNotNull(address) + assertTrue(address!!.isChan) + + address.alias = "Test" + repo.save(address) + + address = repo.getAddress(chan.address) + assertNotNull(address) + assertTrue(address!!.isChan) + assertEquals("Test", address.alias) + } + + @Test + fun `ensure address is removed`() { + val address = repo.getAddress(IDENTITY_A) + repo.remove(address!!) + assertNull(repo.getAddress(IDENTITY_A)) + } + + @Test + fun `ensure address can be retrieved`() { + val address = repo.getAddress(IDENTITY_A) + assertNotNull(address) + assertNotNull(address!!.privateKey) + } + + private fun addSubscription(address: String) { + val subscription = BitmessageAddress(address) + subscription.isSubscribed = true + repo.save(subscription) + } +} diff --git a/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcInventoryTest.kt b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcInventoryTest.kt new file mode 100644 index 0000000..4aa6e69 --- /dev/null +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcInventoryTest.kt @@ -0,0 +1,138 @@ +/* + * 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.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.GetPubkey +import ch.dissem.bitmessage.entity.payload.ObjectPayload +import ch.dissem.bitmessage.entity.payload.ObjectType.GET_PUBKEY +import ch.dissem.bitmessage.entity.payload.ObjectType.MSG +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.ports.Inventory +import ch.dissem.bitmessage.utils.UnixTime.DAY +import ch.dissem.bitmessage.utils.UnixTime.now +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.util.* + +class JdbcInventoryTest : TestBase() { + private lateinit var config: TestJdbcConfig + private lateinit var inventory: Inventory + + private lateinit var inventoryVector1: InventoryVector + private lateinit var inventoryVector2: InventoryVector + private lateinit var inventoryVectorIgnore: InventoryVector + + @Before + fun setUp() { + config = TestJdbcConfig() + config.reset() + + inventory = JdbcInventory(config) + + val object1 = getObjectMessage(1, 300, getPubkey) + inventoryVector1 = object1.inventoryVector + inventory.storeObject(object1) + + val object2 = getObjectMessage(2, 300, getPubkey) + inventoryVector2 = object2.inventoryVector + inventory.storeObject(object2) + + val ignore = getObjectMessage(1, -1 * DAY, getPubkey) + inventoryVectorIgnore = ignore.inventoryVector + inventory.storeObject(ignore) + } + + @Test + fun `ensure inventory can be retrieved`() { + var inventoryVectors = inventory.getInventory(1) + assertEquals(1, inventoryVectors.size.toLong()) + + inventoryVectors = inventory.getInventory(2) + assertEquals(1, inventoryVectors.size.toLong()) + } + + @Test + fun `ensure the IVs of missing objects are returned`() { + val newIV = getObjectMessage(1, 200, getPubkey).inventoryVector + val offer = LinkedList<InventoryVector>() + offer.add(newIV) + offer.add(inventoryVector1) + val missing = inventory.getMissing(offer, 1, 2) + assertEquals(1, missing.size.toLong()) + assertEquals(newIV, missing[0]) + } + + @Test + fun `ensure single object can be retrieved`() { + val `object` = inventory.getObject(inventoryVectorIgnore) + assertNotNull(`object`) + assertEquals(1, `object`!!.stream) + assertEquals(inventoryVectorIgnore, `object`.inventoryVector) + } + + @Test + fun `ensure objects can be retrieved`() { + var objects = inventory.getObjects(1, 4) + assertEquals(2, objects.size.toLong()) + + objects = inventory.getObjects(1, 4, GET_PUBKEY) + assertEquals(2, objects.size.toLong()) + + objects = inventory.getObjects(1, 4, MSG) + assertEquals(0, objects.size.toLong()) + } + + @Test + fun `ensure object can be stored`() { + val `object` = getObjectMessage(5, 0, getPubkey) + inventory.storeObject(`object`) + + assertNotNull(inventory.getObject(`object`.inventoryVector)) + } + + @Test + fun `ensure contained objects are recognized`() { + val `object` = getObjectMessage(5, 0, getPubkey) + + assertFalse(inventory.contains(`object`)) + + inventory.storeObject(`object`) + + assertTrue(inventory.contains(`object`)) + } + + @Test + fun `ensure inventory is cleaned up`() { + assertNotNull(inventory.getObject(inventoryVectorIgnore)) + inventory.cleanup() + assertNull(inventory.getObject(inventoryVectorIgnore)) + } + + private fun getObjectMessage(stream: Long, TTL: Long, payload: ObjectPayload): ObjectMessage { + return ObjectMessage( + nonce = ByteArray(8), + expiresTime = now + TTL, + stream = stream, + payload = payload + ) + } + + private val getPubkey: GetPubkey = GetPubkey(BitmessageAddress("BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt")) +} diff --git a/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.kt b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.kt new file mode 100644 index 0000000..91753f5 --- /dev/null +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.kt @@ -0,0 +1,336 @@ +/* + * 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.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.ports.MessageRepository +import ch.dissem.bitmessage.utils.TestUtils +import ch.dissem.bitmessage.utils.TestUtils.mockedInternalContext +import ch.dissem.bitmessage.utils.UnixTime +import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.Matchers.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.util.* + +class JdbcMessageRepositoryTest : TestBase() { + private lateinit var contactA: BitmessageAddress + private lateinit var contactB: BitmessageAddress + private lateinit var identity: BitmessageAddress + + private lateinit var repo: MessageRepository + + private lateinit var inbox: Label + private lateinit var sent: Label + private lateinit var drafts: Label + private lateinit var unread: Label + + @Before + fun setUp() { + val config = TestJdbcConfig() + config.reset() + val addressRepo = JdbcAddressRepository(config) + repo = JdbcMessageRepository(config) + mockedInternalContext( + cryptography = BouncyCryptography(), + addressRepository = addressRepo, + messageRepository = repo, + port = 12345, + connectionTTL = 10, + connectionLimit = 10 + ) + val tmp = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + contactA = BitmessageAddress(tmp.address) + contactA.pubkey = tmp.pubkey + addressRepo.save(contactA) + contactB = BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj") + addressRepo.save(contactB) + + identity = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + addressRepo.save(identity) + + inbox = repo.getLabels(Label.Type.INBOX)[0] + sent = repo.getLabels(Label.Type.SENT)[0] + drafts = repo.getLabels(Label.Type.DRAFT)[0] + unread = repo.getLabels(Label.Type.UNREAD)[0] + + addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox, unread) + addMessage(identity, contactA, Plaintext.Status.DRAFT, drafts) + addMessage(identity, contactB, Plaintext.Status.DRAFT, unread) + } + + @Test + fun `ensure labels are retrieved`() { + val labels = repo.getLabels() + assertEquals(5, labels.size.toLong()) + } + + @Test + fun `ensure labels can be retrieved by type`() { + val labels = repo.getLabels(Label.Type.INBOX) + assertEquals(1, labels.size.toLong()) + assertEquals("Inbox", labels[0].toString()) + } + + @Test + fun `ensure messages can be found by label`() { + val messages = repo.findMessages(inbox) + assertEquals(1, messages.size.toLong()) + val m = messages[0] + assertEquals(contactA, m.from) + assertEquals(identity, m.to) + assertEquals(Plaintext.Status.RECEIVED, m.status) + } + + @Test + fun `ensure unread messages can be found for all labels`() { + val unread = repo.countUnread(null) + assertThat(unread, `is`(2)) + } + + @Test + fun `ensure unread messages can be found by label`() { + val unread = repo.countUnread(inbox) + assertThat(unread, `is`(1)) + } + + @Test + fun `ensure message can be retrieved by initial hash`() { + val initialHash = ByteArray(64) + val message = repo.findMessages(contactA)[0] + message.initialHash = initialHash + repo.save(message) + val other = repo.getMessage(initialHash) + assertThat<Plaintext>(other, `is`(message)) + } + + @Test + fun `ensure ack message can be updated and retrieved`() { + val initialHash = ByteArray(64) + val message = repo.findMessages(contactA)[0] + message.initialHash = initialHash + val ackMessage = message.ackMessage + repo.save(message) + val other = repo.getMessage(initialHash)!! + assertThat<Plaintext>(other, `is`(message)) + assertThat<ObjectMessage>(other.ackMessage, `is`<ObjectMessage>(ackMessage)) + } + + @Test + fun `ensure messages can be found by status`() { + val messages = repo.findMessages(Plaintext.Status.RECEIVED) + assertEquals(1, messages.size.toLong()) + val m = messages[0] + assertEquals(contactA, m.from) + assertEquals(identity, m.to) + assertEquals(Plaintext.Status.RECEIVED, m.status) + } + + @Test + fun `ensure messages can be found by status and recipient`() { + val messages = repo.findMessages(Plaintext.Status.DRAFT, contactB) + assertEquals(1, messages.size.toLong()) + val m = messages[0] + assertEquals(identity, m.from) + assertEquals(contactB, m.to) + assertEquals(Plaintext.Status.DRAFT, m.status) + } + + @Test + fun `ensure message can be saved`() { + val message = Plaintext.Builder(MSG) + .IV(TestUtils.randomInventoryVector()) + .from(identity) + .to(contactA) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build() + repo.save(message) + + assertNotNull(message.id) + + message.addLabels(inbox) + repo.save(message) + + val messages = repo.findMessages(Plaintext.Status.DOING_PROOF_OF_WORK) + + assertEquals(1, messages.size.toLong()) + assertNotNull(messages[0].inventoryVector) + } + + @Test + fun `ensure message can be updated`() { + var messages = repo.findMessages(Plaintext.Status.DRAFT, contactA) + val message = messages[0] + message.inventoryVector = TestUtils.randomInventoryVector() + repo.save(message) + + messages = repo.findMessages(Plaintext.Status.DRAFT, contactA) + assertEquals(1, messages.size.toLong()) + assertNotNull(messages[0].inventoryVector) + } + + @Test + fun `ensure message is removed`() { + val toRemove = repo.findMessages(Plaintext.Status.DRAFT, contactB)[0] + var messages = repo.findMessages(Plaintext.Status.DRAFT) + assertEquals(2, messages.size.toLong()) + repo.remove(toRemove) + messages = repo.findMessages(Plaintext.Status.DRAFT) + assertThat(messages, hasSize<Plaintext>(1)) + } + + @Test + fun `ensure unacknowledged messages are found for resend`() { + val message = Plaintext.Builder(MSG) + .IV(TestUtils.randomInventoryVector()) + .from(identity) + .to(contactA) + .message("Subject", "Message") + .sent(UnixTime.now) + .status(Plaintext.Status.SENT) + .ttl(2) + .build() + message.updateNextTry() + assertThat(message.retries, `is`(1)) + assertThat<Long>(message.nextTry, greaterThan(UnixTime.now)) + assertThat<Long>(message.nextTry, lessThanOrEqualTo(UnixTime.now + 2)) + repo.save(message) + Thread.sleep(4100) // somewhat longer than 2*TTL + var messagesToResend = repo.findMessagesToResend() + assertThat(messagesToResend, hasSize<Plaintext>(1)) + + message.updateNextTry() + assertThat(message.retries, `is`(2)) + assertThat<Long>(message.nextTry, greaterThan(UnixTime.now)) + repo.save(message) + messagesToResend = repo.findMessagesToResend() + assertThat(messagesToResend, empty<Plaintext>()) + } + + @Test + fun `ensure parents are saved`() { + val parent = storeConversation() + + val responses = repo.findResponses(parent) + assertThat(responses, hasSize<Plaintext>(2)) + assertThat(responses, hasItem(hasMessage("Re: new test", "Nice!"))) + assertThat(responses, hasItem(hasMessage("Re: new test", "PS: it did work!"))) + } + + @Test + fun `ensure conversation can be retrieved`() { + val root = storeConversation() + val conversations = repo.findConversations(inbox) + assertThat(conversations, hasSize<UUID>(2)) + assertThat(conversations, hasItem(root.conversationId)) + } + + private fun addMessage(from: BitmessageAddress, to: BitmessageAddress, status: Plaintext.Status, vararg labels: Label): Plaintext { + val content = Message.Builder() + .subject("Subject") + .body("Message") + .build() + return addMessage(from, to, content, status, *labels) + } + + private fun addMessage(from: BitmessageAddress, to: BitmessageAddress, + content: ExtendedEncoding, status: Plaintext.Status, vararg labels: Label): Plaintext { + val message = Plaintext.Builder(MSG) + .IV(TestUtils.randomInventoryVector()) + .from(from) + .to(to) + .message(content) + .status(status) + .labels(Arrays.asList(*labels)) + .build() + repo.save(message) + return message + } + + private fun storeConversation(): Plaintext { + val older = addMessage(identity, contactA, + Message.Builder() + .subject("hey there") + .body("does it work?") + .build(), + Plaintext.Status.SENT, sent) + + val root = addMessage(identity, contactA, + Message.Builder() + .subject("new test") + .body("There's a new test in town!") + .build(), + Plaintext.Status.SENT, sent) + + addMessage(contactA, identity, + Message.Builder() + .subject("Re: new test") + .body("Nice!") + .addParent(root) + .build(), + Plaintext.Status.RECEIVED, inbox) + + addMessage(contactA, identity, + Message.Builder() + .subject("Re: new test") + .body("PS: it did work!") + .addParent(root) + .addParent(older) + .build(), + Plaintext.Status.RECEIVED, inbox) + + return repo.getMessage(root.id!!) + } + + private fun hasMessage(subject: String?, body: String?): Matcher<Plaintext> { + return object : BaseMatcher<Plaintext>() { + override fun describeTo(description: Description) { + description.appendText("Subject: ").appendText(subject) + description.appendText(", ") + description.appendText("Body: ").appendText(body) + } + + override fun matches(item: Any): Boolean { + if (item is Plaintext) { + if (subject != null && subject != item.subject) { + return false + } + if (body != null && body != item.text) { + return false + } + return true + } else { + return false + } + } + } + } +} diff --git a/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.kt b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.kt new file mode 100644 index 0000000..2b604d4 --- /dev/null +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.kt @@ -0,0 +1,97 @@ +/* + * 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.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.ports.NodeRegistry +import ch.dissem.bitmessage.utils.UnixTime.now +import org.hamcrest.Matchers.empty +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import java.util.* + +/** + * Please note that some tests fail if there is no internet connection, + * as the initial nodes' IP addresses are determined by DNS lookup. + */ +class JdbcNodeRegistryTest : TestBase() { + private lateinit var config: TestJdbcConfig + private lateinit var registry: NodeRegistry + + @Before + fun setUp() { + config = TestJdbcConfig() + config.reset() + registry = JdbcNodeRegistry(config) + + registry.offerAddresses(Arrays.asList( + createAddress(1, 8444, 1, now), + createAddress(2, 8444, 1, now), + createAddress(3, 8444, 1, now), + createAddress(4, 8444, 2, now) + )) + } + + @Test + fun `ensure getKnownNodes() without streams yields empty`() { + assertThat(registry.getKnownAddresses(10), empty<NetworkAddress>()) + } + + @Test + fun `ensure predefined node is returned when database is empty`() { + config.reset() + val knownAddresses = registry.getKnownAddresses(2, 1) + assertEquals(1, knownAddresses.size.toLong()) + } + + @Test + fun `ensure known addresses are retrieved`() { + var knownAddresses = registry.getKnownAddresses(2, 1) + assertEquals(2, knownAddresses.size.toLong()) + + knownAddresses = registry.getKnownAddresses(1000, 1) + assertEquals(3, knownAddresses.size.toLong()) + } + + @Test + fun `ensure offered addresses are added`() { + registry.offerAddresses(Arrays.asList( + createAddress(1, 8444, 1, now), + createAddress(10, 8444, 1, now), + createAddress(11, 8444, 1, now) + )) + + var knownAddresses = registry.getKnownAddresses(1000, 1) + assertEquals(5, knownAddresses.size.toLong()) + + registry.offerAddresses(listOf(createAddress(1, 8445, 1, now))) + + knownAddresses = registry.getKnownAddresses(1000, 1) + assertEquals(6, knownAddresses.size.toLong()) + } + + private fun createAddress(lastByte: Int, port: Int, stream: Long, time: Long): NetworkAddress { + return NetworkAddress.Builder() + .ipv6(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, lastByte) + .port(port) + .stream(stream) + .time(time) + .build() + } +} diff --git a/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.kt b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.kt new file mode 100644 index 0000000..1e08859 --- /dev/null +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.GenericPayload +import ch.dissem.bitmessage.entity.payload.GetPubkey +import ch.dissem.bitmessage.entity.payload.ObjectPayload +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.ports.AddressRepository +import ch.dissem.bitmessage.ports.MessageRepository +import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item +import ch.dissem.bitmessage.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.TestUtils +import ch.dissem.bitmessage.utils.UnixTime +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import org.hamcrest.CoreMatchers.* +import org.junit.Assert.assertThat +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import kotlin.properties.Delegates + +/** + * @author Christian Basler + */ +class JdbcProofOfWorkRepositoryTest : TestBase() { + private var config: TestJdbcConfig by Delegates.notNull<TestJdbcConfig>() + private var repo: JdbcProofOfWorkRepository by Delegates.notNull<JdbcProofOfWorkRepository>() + private var addressRepo: AddressRepository by Delegates.notNull<AddressRepository>() + private var messageRepo: MessageRepository by Delegates.notNull<MessageRepository>() + + private var initialHash1: ByteArray by Delegates.notNull<ByteArray>() + private var initialHash2: ByteArray by Delegates.notNull<ByteArray>() + + @Before + fun setUp() { + config = TestJdbcConfig() + config.reset() + + addressRepo = JdbcAddressRepository(config) + messageRepo = JdbcMessageRepository(config) + repo = JdbcProofOfWorkRepository(config) + TestUtils.mockedInternalContext( + addressRepository = addressRepo, + messageRepository = messageRepo, + proofOfWorkRepository = repo, + cryptography = cryptography() + ) + + repo.putObject(ObjectMessage.Builder() + .payload(GetPubkey(BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(), + 1000, 1000) + initialHash1 = repo.getItems()[0] + + val sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val recipient = TestUtils.loadContact() + addressRepo.save(sender) + addressRepo.save(recipient) + val plaintext = Plaintext.Builder(MSG) + .ackData(cryptography().randomBytes(32)) + .from(sender) + .to(recipient) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build() + messageRepo.save(plaintext) + initialHash2 = cryptography().getInitialHash(plaintext.ackMessage!!) + repo.putObject(Item( + plaintext.ackMessage!!, + 1000, 1000, + UnixTime.now + 10 * MINUTE, + plaintext + )) + } + + @Test + fun `ensure object is stored`() { + val sizeBefore = repo.getItems().size + repo.putObject(ObjectMessage.Builder() + .payload(GetPubkey(BitmessageAddress("BM-2D9U2hv3YBMHM1zERP32anKfVKohyPN9x2"))).build(), + 1000, 1000) + assertThat(repo.getItems().size, `is`(sizeBefore + 1)) + } + + @Test + fun `ensure ack objects are stored`() { + val sizeBefore = repo.getItems().size + val sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val recipient = TestUtils.loadContact() + addressRepo.save(sender) + addressRepo.save(recipient) + val plaintext = Plaintext.Builder(MSG) + .ackData(cryptography().randomBytes(32)) + .from(sender) + .to(recipient) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build() + messageRepo.save(plaintext) + repo.putObject(Item( + plaintext.ackMessage!!, + 1000, 1000, + UnixTime.now + 10 * MINUTE, + plaintext + )) + assertThat(repo.getItems().size, `is`(sizeBefore + 1)) + } + + @Test + fun `ensure item can be retrieved`() { + val item = repo.getItem(initialHash1) + assertThat(item, notNullValue()) + assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GetPubkey::class.java)) + assertThat(item.nonceTrialsPerByte, `is`(1000L)) + assertThat(item.extraBytes, `is`(1000L)) + } + + @Test + fun `ensure ack item can be retrieved`() { + val item = repo.getItem(initialHash2) + assertThat(item, notNullValue()) + assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GenericPayload::class.java)) + assertThat(item.nonceTrialsPerByte, `is`(1000L)) + assertThat(item.extraBytes, `is`(1000L)) + assertThat(item.expirationTime, not<Number>(0)) + assertThat(item.message, notNullValue()) + assertThat<PrivateKey>(item.message?.from?.privateKey, notNullValue()) + assertThat<Pubkey>(item.message?.to?.pubkey, notNullValue()) + } + + @Test(expected = RuntimeException::class) + fun `ensure retrieving nonexisting item causes exception`() { + repo.getItem(ByteArray(0)) + } + + @Test + fun `ensure item can be deleted`() { + repo.removeObject(initialHash1) + repo.removeObject(initialHash2) + assertTrue(repo.getItems().isEmpty()) + } + + @Test + fun `ensure deletion of nonexisting item is handled silently`() { + repo.removeObject(ByteArray(0)) + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/TestBase.kt similarity index 52% rename from core/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java rename to repositories/src/test/kotlin/ch/dissem/bitmessage/repository/TestBase.kt index 91e42b5..cbd6e76 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/CollectionsTest.java +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/TestBase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2017 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,22 +14,20 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.repository -import org.junit.Test; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TestUtils.mockedInternalContext -import java.util.LinkedList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -public class CollectionsTest { - @Test - public void ensureSelectRandomReturnsMaximumPossibleItems() throws Exception { - List<Integer> list = new LinkedList<>(); - for (int i = 0; i < 10; i++) { - list.add(i); +open class TestBase { + companion object { + init { + mockedInternalContext( + cryptography = BouncyCryptography(), + proofOfWorkEngine = MultiThreadedPOWEngine() + ) } - assertEquals(9, Collections.selectRandom(9, list).size()); } -} \ No newline at end of file +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Points.java b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/TestJdbcConfig.kt similarity index 57% rename from core/src/main/java/ch/dissem/bitmessage/utils/Points.java rename to repositories/src/test/kotlin/ch/dissem/bitmessage/repository/TestJdbcConfig.kt index 937fe20..c5c9ec6 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Points.java +++ b/repositories/src/test/kotlin/ch/dissem/bitmessage/repository/TestJdbcConfig.kt @@ -14,19 +14,25 @@ * limitations under the License. */ -package ch.dissem.bitmessage.utils; +package ch.dissem.bitmessage.repository -import java.util.Arrays; +import org.h2.tools.Server +import org.slf4j.LoggerFactory /** - * Created by chris on 20.07.15. + * JdbcConfig to be used for tests. Uses an in-memory database and adds a useful [.reset] method resetting + * the database. */ -public class Points { - public static byte[] getX(byte[] P) { - return Arrays.copyOfRange(P, 1, ((P.length - 1) / 2) + 1); +class TestJdbcConfig : JdbcConfig("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", null) { + + fun reset() { + flyway.clean() + flyway.migrate() } - public static byte[] getY(byte[] P) { - return Arrays.copyOfRange(P, ((P.length - 1) / 2) + 1, P.length); + companion object { + init { + Server.createTcpServer().start() + } } } diff --git a/wif/build.gradle b/wif/build.gradle index a466f69..aa7cbb9 100644 --- a/wif/build.gradle +++ b/wif/build.gradle @@ -12,8 +12,8 @@ uploadArchives { dependencies { compile project(':core') - compile 'org.ini4j:ini4j:0.5.4' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.21' + compile 'org.ini4j:ini4j' + testCompile 'junit:junit' + testCompile 'com.nhaarman:mockito-kotlin' 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 deleted file mode 100644 index 3f4b370..0000000 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java +++ /dev/null @@ -1,106 +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.wif; - -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.exception.ApplicationException; -import ch.dissem.bitmessage.utils.Base58; -import org.ini4j.Ini; -import org.ini4j.Profile; - -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.cryptography; - -/** - * @author Christian Basler - */ -public class WifExporter { - private final BitmessageContext ctx; - private final Ini ini; - - public WifExporter(BitmessageContext ctx) { - this.ctx = ctx; - this.ini = new Ini(); - } - - public WifExporter addAll() { - for (BitmessageAddress identity : ctx.addresses().getIdentities()) { - addIdentity(identity); - } - return this; - } - - public WifExporter addAll(Collection<BitmessageAddress> identities) { - for (BitmessageAddress identity : identities) { - addIdentity(identity); - } - return this; - } - - public WifExporter addIdentity(BitmessageAddress identity) { - Profile.Section section = ini.add(identity.getAddress()); - section.add("label", identity.getAlias()); - section.add("enabled", true); - section.add("decoy", false); - if (identity.isChan()) { - section.add("chan", identity.isChan()); - } - section.add("noncetrialsperbyte", identity.getPubkey().getNonceTrialsPerByte()); - section.add("payloadlengthextrabytes", identity.getPubkey().getExtraBytes()); - section.add("privsigningkey", exportSecret(identity.getPrivateKey().getPrivateSigningKey())); - section.add("privencryptionkey", exportSecret(identity.getPrivateKey().getPrivateEncryptionKey())); - return this; - } - - private String exportSecret(byte[] privateKey) { - if (privateKey.length != PRIVATE_KEY_SIZE) { - throw new IllegalArgumentException("Private key of length 32 expected, but was " + privateKey.length); - } - byte[] result = new byte[37]; - result[0] = (byte) 0x80; - System.arraycopy(privateKey, 0, result, 1, PRIVATE_KEY_SIZE); - byte[] hash = cryptography().doubleSha256(result, PRIVATE_KEY_SIZE + 1); - System.arraycopy(hash, 0, result, PRIVATE_KEY_SIZE + 1, 4); - return Base58.encode(result); - } - - public void write(File file) throws IOException { - file.createNewFile(); - try (FileOutputStream out = new FileOutputStream(file)) { - write(out); - } - } - - public void write(OutputStream out) throws IOException { - ini.store(out); - } - - @Override - public String toString() { - StringWriter writer = new StringWriter(); - try { - ini.store(writer); - } catch (IOException e) { - throw new ApplicationException(e); - } - return writer.toString(); - } -} diff --git a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java b/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java deleted file mode 100644 index 0d010c9..0000000 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java +++ /dev/null @@ -1,119 +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.wif; - -import ch.dissem.bitmessage.BitmessageContext; -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 org.ini4j.Ini; -import org.ini4j.Profile; - -import java.io.*; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Map.Entry; - -import static ch.dissem.bitmessage.utils.Singleton.cryptography; - -/** - * @author Christian Basler - */ -public class WifImporter { - private static final byte WIF_FIRST_BYTE = (byte) 0x80; - private static final int WIF_SECRET_LENGTH = 37; - - private final BitmessageContext ctx; - private final List<BitmessageAddress> identities = new LinkedList<>(); - - public WifImporter(BitmessageContext ctx, File file) throws IOException { - this(ctx, new FileInputStream(file)); - } - - public WifImporter(BitmessageContext ctx, String data) throws IOException { - this(ctx, new ByteArrayInputStream(data.getBytes("utf-8"))); - } - - public WifImporter(BitmessageContext ctx, InputStream in, Pubkey.Feature... features) throws IOException { - this.ctx = ctx; - - Ini ini = new Ini(); - ini.load(in); - - for (Entry<String, Profile.Section> entry : ini.entrySet()) { - if (!entry.getKey().startsWith("BM-")) - continue; - - Profile.Section section = entry.getValue(); - BitmessageAddress address = Factory.createIdentityFromPrivateKey( - entry.getKey(), - getSecret(section.get("privsigningkey")), - getSecret(section.get("privencryptionkey")), - Long.parseLong(section.get("noncetrialsperbyte")), - Long.parseLong(section.get("payloadlengthextrabytes")), - Pubkey.Feature.bitfield(features) - ); - if (section.containsKey("chan")) { - address.setChan(Boolean.parseBoolean(section.get("chan"))); - } - address.setAlias(section.get("label")); - identities.add(address); - } - } - - private byte[] getSecret(String walletImportFormat) throws IOException { - byte[] bytes = Base58.decode(walletImportFormat); - if (bytes[0] != WIF_FIRST_BYTE) - throw new IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + - " was " + bytes[0]); - if (bytes.length != WIF_SECRET_LENGTH) - throw new IOException("Unknown format: " + WIF_SECRET_LENGTH + - " bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); - - byte[] hash = cryptography().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); - } - return Arrays.copyOfRange(bytes, 1, 33); - } - - public List<BitmessageAddress> getIdentities() { - return identities; - } - - public WifImporter importAll() { - for (BitmessageAddress identity : identities) { - ctx.addresses().save(identity); - } - return this; - } - - public WifImporter importAll(Collection<BitmessageAddress> identities) { - for (BitmessageAddress identity : identities) { - ctx.addresses().save(identity); - } - return this; - } - - public WifImporter importIdentity(BitmessageAddress identity) { - ctx.addresses().save(identity); - return this; - } -} diff --git a/wif/src/main/kotlin/ch/dissem/bitmessage/wif/WifExporter.kt b/wif/src/main/kotlin/ch/dissem/bitmessage/wif/WifExporter.kt new file mode 100644 index 0000000..9245ba7 --- /dev/null +++ b/wif/src/main/kotlin/ch/dissem/bitmessage/wif/WifExporter.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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. + */ + +/* + * 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.wif + +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.valueobject.PrivateKey.Companion.PRIVATE_KEY_SIZE +import ch.dissem.bitmessage.utils.Base58 +import ch.dissem.bitmessage.utils.Singleton.cryptography +import org.ini4j.Ini +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream +import java.io.StringWriter + +/** + * @author Christian Basler + */ +class WifExporter(private val ctx: BitmessageContext) { + private val ini = Ini() + + fun addAll(): WifExporter { + ctx.addresses.getIdentities().forEach { addIdentity(it) } + return this + } + + fun addAll(identities: Collection<BitmessageAddress>): WifExporter { + identities.forEach { addIdentity(it) } + return this + } + + fun addIdentity(identity: BitmessageAddress): WifExporter { + val section = ini.add(identity.address) + section.add("label", identity.alias) + section.add("enabled", true) + section.add("decoy", false) + if (identity.isChan) { + section.add("chan", identity.isChan) + } + section.add("noncetrialsperbyte", identity.pubkey!!.nonceTrialsPerByte) + section.add("payloadlengthextrabytes", identity.pubkey!!.extraBytes) + section.add("privsigningkey", exportSecret(identity.privateKey!!.privateSigningKey)) + section.add("privencryptionkey", exportSecret(identity.privateKey!!.privateEncryptionKey)) + return this + } + + private fun exportSecret(privateKey: ByteArray): String { + if (privateKey.size != PRIVATE_KEY_SIZE) { + throw IllegalArgumentException("Private key of length 32 expected, but was " + privateKey.size) + } + val result = ByteArray(37) + result[0] = 0x80.toByte() + System.arraycopy(privateKey, 0, result, 1, PRIVATE_KEY_SIZE) + val hash = cryptography().doubleSha256(result, PRIVATE_KEY_SIZE + 1) + System.arraycopy(hash, 0, result, PRIVATE_KEY_SIZE + 1, 4) + return Base58.encode(result) + } + + fun write(file: File) { + file.createNewFile() + FileOutputStream(file).use { out -> write(out) } + } + + fun write(out: OutputStream) { + ini.store(out) + } + + override fun toString(): String { + val writer = StringWriter() + ini.store(writer) + return writer.toString() + } +} diff --git a/wif/src/main/kotlin/ch/dissem/bitmessage/wif/WifImporter.kt b/wif/src/main/kotlin/ch/dissem/bitmessage/wif/WifImporter.kt new file mode 100644 index 0000000..5b7e958 --- /dev/null +++ b/wif/src/main/kotlin/ch/dissem/bitmessage/wif/WifImporter.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2017 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. + */ + +/* + * 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.wif + +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.Base58 +import ch.dissem.bitmessage.utils.Singleton.cryptography +import org.ini4j.Ini +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.util.* + +/** + * @author Christian Basler + */ +class WifImporter constructor( + private val ctx: BitmessageContext, + `in`: InputStream, + vararg features: Pubkey.Feature +) { + private val identities = LinkedList<BitmessageAddress>() + + constructor(ctx: BitmessageContext, file: File) : this(ctx, FileInputStream(file)) + + constructor(ctx: BitmessageContext, data: String) : this(ctx, ByteArrayInputStream(data.toByteArray(charset("utf-8")))) + + init { + val ini = Ini() + ini.load(`in`) + + for ((key, section) in ini) { + if (!key.startsWith("BM-")) + continue + + val address = Factory.createIdentityFromPrivateKey( + key, + getSecret(section["privsigningkey"] ?: throw ApplicationException("privsigningkey missing for $key")), + getSecret(section["privencryptionkey"] ?: throw ApplicationException("privencryptionkey missing for $key")), + section["noncetrialsperbyte"]?.toLongOrNull() ?: throw ApplicationException("noncetrialsperbyte missing for $key"), + section["payloadlengthextrabytes"]?.toLongOrNull() ?: throw ApplicationException("payloadlengthextrabytes missing for $key"), + Pubkey.Feature.bitfield(*features) + ) + if (section.containsKey("chan")) { + address.isChan = java.lang.Boolean.parseBoolean(section["chan"]) + } + address.alias = section["label"] + identities.add(address) + } + } + + private fun getSecret(walletImportFormat: String): ByteArray { + val bytes = Base58.decode(walletImportFormat) + if (bytes[0] != WIF_FIRST_BYTE) + throw ApplicationException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + + " was " + bytes[0]) + if (bytes.size != WIF_SECRET_LENGTH) + throw ApplicationException("Unknown format: " + WIF_SECRET_LENGTH + + " bytes expected, but secret " + walletImportFormat + " was " + bytes.size + " long") + + val hash = cryptography().doubleSha256(bytes, 33) + (0..3) + .filter { hash[it] != bytes[33 + it] } + .forEach { throw ApplicationException("Hash check failed for secret " + walletImportFormat) } + return Arrays.copyOfRange(bytes, 1, 33) + } + + fun getIdentities(): List<BitmessageAddress> { + return identities + } + + fun importAll(): WifImporter { + identities.forEach { ctx.addresses.save(it) } + return this + } + + fun importAll(identities: Collection<BitmessageAddress>): WifImporter { + identities.forEach { ctx.addresses.save(it) } + return this + } + + fun importIdentity(identity: BitmessageAddress): WifImporter { + ctx.addresses.save(identity) + return this + } + + companion object { + private const val WIF_FIRST_BYTE = 0x80.toByte() + private const val WIF_SECRET_LENGTH = 37 + } +} diff --git a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java b/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java deleted file mode 100644 index 225f2f9..0000000 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java +++ /dev/null @@ -1,107 +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.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; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class WifExporterTest { - private AddressRepository repo = mock(AddressRepository.class); - private BitmessageContext ctx; - private WifImporter importer; - private WifExporter exporter; - - @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(); - importer = new WifImporter(ctx, getClass().getClassLoader().getResourceAsStream("nuked.dat")); - assertEquals(81, importer.getIdentities().size()); - exporter = new WifExporter(ctx); - } - - @Test - public void testAddAll() throws Exception { - when(repo.getIdentities()).thenReturn(importer.getIdentities()); - exporter.addAll(); - String result = exporter.toString(); - int count = 0; - for (int i = 0; i < result.length(); i++) { - if (result.charAt(i) == '[') count++; - } - assertEquals(importer.getIdentities().size(), count); - } - - @Test - public void testAddAllFromCollection() throws Exception { - exporter.addAll(importer.getIdentities()); - String result = exporter.toString(); - int count = 0; - for (int i = 0; i < result.length(); i++) { - if (result.charAt(i) == '[') count++; - } - assertEquals(importer.getIdentities().size(), count); - } - - @Test - public void testAddIdentity() throws Exception { - String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]" + System.lineSeparator() + - "label = Nuked Address" + System.lineSeparator() + - "enabled = true" + System.lineSeparator() + - "decoy = false" + System.lineSeparator() + - "noncetrialsperbyte = 320" + System.lineSeparator() + - "payloadlengthextrabytes = 14000" + System.lineSeparator() + - "privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9" + System.lineSeparator() + - "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck" + System.lineSeparator() + - System.lineSeparator(); - importer = new WifImporter(ctx, expected); - exporter.addIdentity(importer.getIdentities().get(0)); - assertEquals(expected, exporter.toString()); - } - - @Test - public void ensureChanIsAdded() throws Exception { - String expected = "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]" + System.lineSeparator() + - "label = general" + System.lineSeparator() + - "enabled = true" + System.lineSeparator() + - "decoy = false" + System.lineSeparator() + - "chan = true" + System.lineSeparator() + - "noncetrialsperbyte = 1000" + System.lineSeparator() + - "payloadlengthextrabytes = 1000" + System.lineSeparator() + - "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ" + System.lineSeparator() + - "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq" + System.lineSeparator() + - System.lineSeparator(); - BitmessageAddress chan = ctx.joinChan("general", "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r"); - exporter.addIdentity(chan); - assertEquals(expected, exporter.toString()); - } -} \ No newline at end of file diff --git a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java b/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java deleted file mode 100644 index 398ff20..0000000 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java +++ /dev/null @@ -1,116 +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.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; - -import java.util.List; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -public class WifImporterTest { - private AddressRepository repo = mock(AddressRepository.class); - private BitmessageContext ctx; - private WifImporter importer; - - @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(); - importer = new WifImporter(ctx, getClass().getClassLoader().getResourceAsStream("nuked.dat")); - } - - - @Test - public void testImportSingleIdentity() throws Exception { - importer = new WifImporter(ctx, "[BM-2cWJ4UFRTCehWuWNsW8fJkAYMxU4S8jxci]\n" + - "label = Nuked Address\n" + - "enabled = true\n" + - "decoy = false\n" + - "noncetrialsperbyte = 320\n" + - "payloadlengthextrabytes = 14000\n" + - "privsigningkey = 5JU5t2JA58sP5aJwKAcrYg5EpBA9bJPrBSaFfaZ7ogmwTMDCfHL\n" + - "privencryptionkey = 5Kkx5MwjQcM4kyduKvCEPM6nVNynMdRcg88VQ5iVDWUekMz1igH"); - assertEquals(1, importer.getIdentities().size()); - BitmessageAddress identity = importer.getIdentities().get(0); - assertEquals("BM-2cWJ4UFRTCehWuWNsW8fJkAYMxU4S8jxci", identity.getAddress()); - assertEquals("Nuked Address", identity.getAlias()); - assertEquals(320, identity.getPubkey().getNonceTrialsPerByte()); - assertEquals(14000, identity.getPubkey().getExtraBytes()); - assertNotNull("Private key", identity.getPrivateKey()); - assertEquals(32, identity.getPrivateKey().getPrivateEncryptionKey().length); - assertEquals(32, identity.getPrivateKey().getPrivateSigningKey().length); - assertFalse(identity.isChan()); - } - - @Test - public void testGetIdentities() throws Exception { - List<BitmessageAddress> identities = importer.getIdentities(); - assertEquals(81, identities.size()); - } - - @Test - public void testImportAll() throws Exception { - importer.importAll(); - verify(repo, times(81)).save(any(BitmessageAddress.class)); - } - - @Test - public void testImportAllFromCollection() throws Exception { - List<BitmessageAddress> identities = importer.getIdentities(); - importer.importAll(identities); - for (BitmessageAddress identity : identities) { - verify(repo, times(1)).save(identity); - } - } - - @Test - public void testImportIdentity() throws Exception { - List<BitmessageAddress> identities = importer.getIdentities(); - importer.importIdentity(identities.get(0)); - verify(repo, times(1)).save(identities.get(0)); - } - - @Test - public void ensureChanIsImported() throws Exception { - importer = new WifImporter(ctx, "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]\n" + - "label = [chan] general\n" + - "enabled = true\n" + - "decoy = false\n" + - "chan = true\n" + - "noncetrialsperbyte = 1000\n" + - "payloadlengthextrabytes = 1000\n" + - "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ\n" + - "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq\n"); - assertEquals(1, importer.getIdentities().size()); - BitmessageAddress chan = importer.getIdentities().get(0); - assertTrue(chan.isChan()); - } -} \ No newline at end of file diff --git a/wif/src/test/kotlin/ch/dissem/bitmessage/wif/WifExporterTest.kt b/wif/src/test/kotlin/ch/dissem/bitmessage/wif/WifExporterTest.kt new file mode 100644 index 0000000..657ec48 --- /dev/null +++ b/wif/src/test/kotlin/ch/dissem/bitmessage/wif/WifExporterTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2017 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. + */ + +/* + * 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.wif + +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.ports.AddressRepository +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class WifExporterTest { + private val repo = mock<AddressRepository>() + private lateinit var ctx: BitmessageContext + private lateinit var importer: WifImporter + private lateinit var exporter: WifExporter + + @Before + fun setUp() { + ctx = BitmessageContext.Builder() + .cryptography(BouncyCryptography()) + .networkHandler(mock()) + .inventory(mock()) + .messageRepo(mock()) + .powRepo(mock()) + .nodeRegistry(mock()) + .addressRepo(repo) + .listener { } + .build() + importer = WifImporter(ctx, javaClass.classLoader.getResourceAsStream("nuked.dat")) + assertEquals(81, importer.getIdentities().size) + exporter = WifExporter(ctx) + } + + @Test + fun `ensure all identities in context are added`() { + whenever(repo.getIdentities()).thenReturn(importer.getIdentities()) + exporter.addAll() + val result = exporter.toString() + var count = 0 + for (i in 0..result.length - 1) { + if (result[i] == '[') count++ + } + assertEquals(importer.getIdentities().size, count) + } + + @Test + fun `ensure all from a collection are added`() { + exporter.addAll(importer.getIdentities()) + val result = exporter.toString() + var count = 0 + for (i in 0..result.length - 1) { + if (result[i] == '[') count++ + } + assertEquals(importer.getIdentities().size, count) + } + + @Test + fun `ensure identity is added`() { + val 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 = WifImporter(ctx, expected) + exporter.addIdentity(importer.getIdentities()[0]) + assertEquals(expected, exporter.toString()) + } + + @Test + fun `ensure chan is added`() { + val expected = "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]" + System.lineSeparator() + + "label = general" + System.lineSeparator() + + "enabled = true" + System.lineSeparator() + + "decoy = false" + System.lineSeparator() + + "chan = true" + System.lineSeparator() + + "noncetrialsperbyte = 1000" + System.lineSeparator() + + "payloadlengthextrabytes = 1000" + System.lineSeparator() + + "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ" + System.lineSeparator() + + "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq" + System.lineSeparator() + + System.lineSeparator() + val chan = ctx.joinChan("general", "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r") + exporter.addIdentity(chan) + assertEquals(expected, exporter.toString()) + } +} diff --git a/wif/src/test/kotlin/ch/dissem/bitmessage/wif/WifImporterTest.kt b/wif/src/test/kotlin/ch/dissem/bitmessage/wif/WifImporterTest.kt new file mode 100644 index 0000000..08b14fa --- /dev/null +++ b/wif/src/test/kotlin/ch/dissem/bitmessage/wif/WifImporterTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2017 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. + */ + +/* + * 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.wif + +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.ports.AddressRepository +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class WifImporterTest { + private val repo = mock<AddressRepository>() + private lateinit var ctx: BitmessageContext + private lateinit var importer: WifImporter + + @Before + fun setUp() { + ctx = BitmessageContext.Builder() + .cryptography(BouncyCryptography()) + .networkHandler(mock()) + .inventory(mock()) + .messageRepo(mock()) + .powRepo(mock()) + .nodeRegistry(mock()) + .addressRepo(repo) + .listener { } + .build() + importer = WifImporter(ctx, javaClass.classLoader.getResourceAsStream("nuked.dat")) + } + + + @Test + fun `ensure single identity is imported`() { + importer = WifImporter(ctx, "[BM-2cWJ4UFRTCehWuWNsW8fJkAYMxU4S8jxci]\n" + + "label = Nuked Address\n" + + "enabled = true\n" + + "decoy = false\n" + + "noncetrialsperbyte = 320\n" + + "payloadlengthextrabytes = 14000\n" + + "privsigningkey = 5JU5t2JA58sP5aJwKAcrYg5EpBA9bJPrBSaFfaZ7ogmwTMDCfHL\n" + + "privencryptionkey = 5Kkx5MwjQcM4kyduKvCEPM6nVNynMdRcg88VQ5iVDWUekMz1igH") + assertEquals(1, importer.getIdentities().size) + val identity = importer.getIdentities()[0] + assertEquals("BM-2cWJ4UFRTCehWuWNsW8fJkAYMxU4S8jxci", identity.address) + assertEquals("Nuked Address", identity.alias) + assertEquals(320L, identity.pubkey?.nonceTrialsPerByte) + assertEquals(14000L, identity.pubkey?.extraBytes) + assertNotNull("Private key", identity.privateKey) + assertEquals(32, identity.privateKey?.privateEncryptionKey?.size) + assertEquals(32, identity.privateKey?.privateSigningKey?.size) + assertFalse(identity.isChan) + } + + @Test + fun `ensure all identities are retrieved`() { + val identities = importer.getIdentities() + assertEquals(81, identities.size) + } + + @Test + fun `ensure all identities are imported`() { + importer.importAll() + verify(repo, times(81)).save(any()) + } + + @Test + fun `ensure all identities in collection are imported`() { + val identities = importer.getIdentities() + importer.importAll(identities) + for (identity in identities) { + verify(repo, times(1)).save(identity) + } + } + + @Test + fun `ensure single identity from list is imported`() { + val identities = importer.getIdentities() + importer.importIdentity(identities[0]) + verify(repo, times(1)).save(identities[0]) + } + + @Test + fun `ensure chan is imported`() { + importer = WifImporter(ctx, "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]\n" + + "label = [chan] general\n" + + "enabled = true\n" + + "decoy = false\n" + + "chan = true\n" + + "noncetrialsperbyte = 1000\n" + + "payloadlengthextrabytes = 1000\n" + + "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ\n" + + "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq\n") + assertEquals(1, importer.getIdentities().size) + val chan = importer.getIdentities()[0] + assertTrue(chan.isChan) + } +}