Migrated BitmessageContext and fixed some tests

This commit is contained in:
Christian Basler 2017-06-08 16:59:24 +02:00
parent fa0e53289c
commit 83e50e1ad1
12 changed files with 872 additions and 831 deletions

View File

@ -1,452 +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;
/**
* <p>Use this class if you want to create a Bitmessage client.</p>
* You'll need the Builder to create a BitmessageContext, and set the following properties:
* <ul>
* <li>addressRepo</li>
* <li>inventory</li>
* <li>nodeRegistry</li>
* <li>networkHandler</li>
* <li>messageRepo</li>
* <li>streams</li>
* </ul>
* <p>The default implementations in the different module builds can be used.</p>
* <p>The port defaults to 8444 (the default Bitmessage port)</p>
*/
public class BitmessageContext {
public static final int CURRENT_VERSION = 3;
private final static Logger LOG = LoggerFactory.getLogger(BitmessageContext.class);
private final InternalContext ctx;
private final Labeler labeler;
private final boolean sendPubkeyOnIdentityCreation;
private BitmessageContext(Builder builder) {
ctx = new InternalContext(
builder.cryptography,
builder.inventory,
builder.nodeRegistry,
builder.networkHandler,
builder.addressRepo,
builder.messageRepo,
builder.proofOfWorkRepository,
builder.proofOfWorkEngine,
builder.customCommandHandler,
builder.listener,
builder.labeler,
builder.port,
builder.connectionTTL,
builder.connectionLimit
);
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.Companion.chan(address, passphrase);
chan.setAlias(passphrase);
ctx.getAddressRepository().save(chan);
return chan;
}
public BitmessageAddress createChan(String passphrase) {
// FIXME: hardcoded stream number
BitmessageAddress chan = BitmessageAddress.Companion.chan(1, passphrase);
ctx.getAddressRepository().save(chan);
return chan;
}
public List<BitmessageAddress> createDeterministicAddresses(
String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) {
List<BitmessageAddress> result = BitmessageAddress.Companion.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().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.
* <p>
* You should call this method regularly, but be aware of the following:
* <ul>
* <li>As messages might be sent, POW will be done. It is therefore not advised to
* call it on shutdown.</li>
* <li>It shouldn't be called right after startup, as it's possible the missing
* acknowledgement was sent while the client was offline.</li>
* <li>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.</li>
* </ul>
*/
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.Companion.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");
}
}
}

View File

@ -0,0 +1,451 @@
/*
* 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 private constructor(builder: BitmessageContext.Builder) {
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
init {
labeler = builder.labeler ?: DefaultLabeler()
internals = InternalContext(
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? {
LOG.debug("Received custom request, but no custom command handler configured.")
return null
}
},
builder.listener,
labeler,
builder.port,
builder.connectionTTL,
builder.connectionLimit
)
internals.proofOfWorkService.doMissingProofOfWork(30000) // TODO: this should be configurable
sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation
(builder.listener as? Listener.WithContext)?.setContext(this)
}
val addresses: AddressRepository = internals.addressRepository
@JvmName("addresses") get
val messages: MessageRepository = internals.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) {
internals.addressRepository.getAddress(contact.address)?.let {
if (it.pubkey == null) {
internals.requestPubkey(contact)
}
}
}
}
fun addSubscribtion(address: BitmessageAddress) {
address.isSubscribed = true
internals.addressRepository.save(address)
tryToFindBroadcastsForAddress(address)
}
private fun tryToFindBroadcastsForAddress(address: BitmessageAddress) {
for (`object` in internals.inventory.getObjects(address.stream, Broadcast.getVersion(address), ObjectType.BROADCAST)) {
try {
val broadcast = `object`.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(`object`)
} catch (ignore: DecryptionFailedException) {
} catch (e: Exception) {
LOG.debug(e.message, e)
}
}
}
fun status(): Property {
return Property("status",
internals.networkHandler.networkStatus,
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 listener by Delegates.notNull<Listener>()
internal var connectionLimit = 150
internal var connectionTTL = 30 * MINUTE
internal var sendPubkeyOnIdentityCreation = true
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
}
fun connectionLimit(connectionLimit: Int): Builder {
this.connectionLimit = connectionLimit
return this
}
fun connectionTTL(hours: Int): Builder {
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.
*/
fun doNotSendPubkeyOnIdentityCreation(): Builder {
this.sendPubkeyOnIdentityCreation = false
return this
}
fun build(): BitmessageContext {
nonNull("inventory", inventory)
nonNull("nodeRegistry", nodeRegistry)
nonNull("networkHandler", networkHandler)
nonNull("addressRepo", addressRepo)
nonNull("messageRepo", messageRepo)
nonNull("proofOfWorkRepo", proofOfWorkRepository)
return BitmessageContext(this)
}
private fun nonNull(name: String, o: Any?) {
if (o == null) throw IllegalStateException(name + " must not be null")
}
}
companion object {
@JvmField val CURRENT_VERSION = 3
private val LOG = LoggerFactory.getLogger(BitmessageContext::class.java)
}
}

View File

@ -16,8 +16,7 @@
package ch.dissem.bitmessage.entity
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE
import ch.dissem.bitmessage.entity.Plaintext.Encoding.*
import ch.dissem.bitmessage.entity.payload.Msg
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
@ -36,6 +35,13 @@ import java.util.*
import java.util.Collections
import kotlin.collections.HashSet
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)
}
/**
* The unencrypted message to be sent by 'msg' or 'broadcast'.
*/
@ -182,9 +188,39 @@ class Plaintext private constructor(
status
)
constructor(
type: Type,
from: BitmessageAddress,
to: BitmessageAddress? = null,
encoding: Encoding = SIMPLE,
subject: String,
body: String,
ackData: ByteArray = cryptography().randomBytes(Msg.ACK_LENGTH),
conversationId: UUID = UUID.randomUUID(),
ttl: Long = TTL.msg,
labels: MutableSet<Label> = HashSet(),
status: Status = Status.DRAFT
) : this(
type,
from,
to,
encoding.code,
message(encoding, subject, body),
ackData,
lazy { Factory.createAck(from, ackData, ttl) },
conversationId,
null,
null,
null,
null,
ttl,
labels,
status
)
constructor(builder: Builder) : this(
builder.type,
builder.from!!,
builder.from ?: throw IllegalStateException("sender identity not set"),
builder.to,
builder.encoding,
builder.message,

View File

@ -19,6 +19,7 @@ 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 {
@ -26,7 +27,7 @@ open class DefaultLabeler : Labeler {
override fun setLabels(msg: Plaintext) {
msg.status = RECEIVED
if (msg.type === Plaintext.Type.BROADCAST) {
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))

View File

@ -114,12 +114,12 @@ class BitmessageContextTest {
@Test
fun `ensure contact is saved and pubkey requested`() {
val contact = BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT")
whenever(ctx.addresses().getAddress(contact.address)).thenReturn(contact)
whenever(ctx.addresses.getAddress(contact.address)).thenReturn(contact)
ctx.addContact(contact)
verify(ctx.addresses(), timeout(1000).atLeastOnce()).save(contact)
verify(ctx.internals().proofOfWorkEngine, timeout(1000)).calculateNonce(any(), any(), any())
verify(ctx.addresses, timeout(1000).atLeastOnce()).save(contact)
verify(ctx.internals.proofOfWorkEngine, timeout(1000)).calculateNonce(any(), any(), any())
}
@Test
@ -131,8 +131,8 @@ class BitmessageContextTest {
ctx.addContact(contact)
verify(ctx.addresses(), times(1)).save(contact)
verify(ctx.internals().proofOfWorkEngine, never()).calculateNonce(any(), any(), any())
verify(ctx.addresses, times(1)).save(contact)
verify(ctx.internals.proofOfWorkEngine, never()).calculateNonce(any(), any(), any())
}
@Test
@ -150,12 +150,12 @@ class BitmessageContextTest {
)
val contact = BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT")
whenever(ctx.addresses().getAddress(contact.address)).thenReturn(contact)
whenever(ctx.addresses.getAddress(contact.address)).thenReturn(contact)
ctx.addContact(contact)
verify(ctx.addresses(), atLeastOnce()).save(contact)
verify(ctx.internals().proofOfWorkEngine, never()).calculateNonce(any(), any(), any())
verify(ctx.addresses, atLeastOnce()).save(contact)
verify(ctx.internals.proofOfWorkEngine, never()).calculateNonce(any(), any(), any())
}
@Test
@ -174,12 +174,12 @@ class BitmessageContextTest {
val contact = BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h")
val stored = BitmessageAddress(contact.address)
stored.alias = "Test"
whenever(ctx.addresses().getAddress(contact.address)).thenReturn(stored)
whenever(ctx.addresses.getAddress(contact.address)).thenReturn(stored)
ctx.addContact(contact)
verify(ctx.addresses(), atLeastOnce()).save(any())
verify(ctx.internals().proofOfWorkEngine, never()).calculateNonce(any(), any(), any())
verify(ctx.addresses, atLeastOnce()).save(any())
verify(ctx.internals.proofOfWorkEngine, never()).calculateNonce(any(), any(), any())
}
@Test
@ -191,12 +191,12 @@ class BitmessageContextTest {
"V5Broadcast.payload"
)
whenever(ctx.addresses().getSubscriptions(any())).thenReturn(listOf(address))
whenever(ctx.addresses.getSubscriptions(any())).thenReturn(listOf(address))
ctx.addSubscribtion(address)
verify(ctx.addresses(), atLeastOnce()).save(address)
verify(ctx.addresses, atLeastOnce()).save(address)
assertThat(address.isSubscribed, `is`(true))
verify(ctx.internals().inventory).getObjects(eq(address.stream), any(), any())
verify(ctx.internals.inventory).getObjects(eq(address.stream), any(), any())
verify(listener).receive(any())
}
@ -210,9 +210,9 @@ class BitmessageContextTest {
ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(),
"Subject", "Message")
assertEquals(2, testPowRepo.added)
verify(ctx.internals().proofOfWorkRepository, timeout(10000).atLeastOnce())
verify(ctx.internals.proofOfWorkRepository, timeout(10000).atLeastOnce())
.putObject(argThat { payload.type == ObjectType.MSG }, eq(1000L), eq(1000L))
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG })
verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG })
}
@Test
@ -222,7 +222,7 @@ class BitmessageContextTest {
"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 })
verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG })
}
@Test(expected = IllegalArgumentException::class)
@ -236,11 +236,11 @@ class BitmessageContextTest {
fun `ensure broadcast is sent`() {
ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"),
"Subject", "Message")
verify(ctx.internals().proofOfWorkRepository, timeout(10000).atLeastOnce())
verify(ctx.internals.proofOfWorkRepository, timeout(10000).atLeastOnce())
.putObject(argThat { payload.type == ObjectType.BROADCAST }, eq(1000L), eq(1000L))
verify(ctx.internals().proofOfWorkEngine)
verify(ctx.internals.proofOfWorkEngine)
.calculateNonce(any(), any(), any())
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(argThat { type == Type.BROADCAST })
verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.BROADCAST })
}
@Test(expected = IllegalArgumentException::class)
@ -311,9 +311,9 @@ class BitmessageContextTest {
.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)
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))
verify(ctx.labeler, timeout(1000).times(1)).markAsSent(eq(plaintext))
}
}

View File

@ -15,4 +15,5 @@ dependencies {
compile 'org.bouncycastle:bcprov-jdk15on:1.56'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.7.21'
testCompile project(path: ':core', configuration: 'testArtifacts')
}

View File

@ -1,172 +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.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());
}
@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.Companion.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.Companion.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));
}
}

View File

@ -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)
}
}
}

View File

@ -15,4 +15,5 @@ dependencies {
compile 'com.madgag.spongycastle:prov:1.54.0.0'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.7.21'
testCompile project(path: ':core', configuration: 'testArtifacts')
}

View File

@ -1,174 +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.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.Assert;
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);
InternalContext.Companion.setValue(null, null, ctx);
when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine());
}
@Test
public void testRipemd160() {
Assert.assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE));
}
@Test
public void testSha1() {
Assert.assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE));
}
@Test
public void testSha512() {
Assert.assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE));
}
@Test
public void testChaining() {
Assert.assertArrayEquals(TEST_SHA512, crypto.sha512("test".getBytes(), "string".getBytes()));
}
@Test
public void ensureDoubleHashYieldsSameResultAsHashOfHash() {
Assert.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.Companion.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.Companion.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));
}
}

View File

@ -0,0 +1,177 @@
/*
* 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.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.Assert
import org.junit.BeforeClass
import org.junit.Test
import javax.xml.bind.DatatypeConverter
import java.io.ByteArrayInputStream
import java.io.IOException
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.mockito.Mockito.mock
import org.mockito.Mockito.`when`
/**
* @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)
}
}
}

View File

@ -130,14 +130,14 @@ public class JdbcInventoryTest extends TestBase {
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();
.nonce(new byte[8])
.expiresTime(now() + TTL)
.stream(stream)
.payload(payload)
.build();
}
private GetPubkey getGetPubkey() {
return new GetPubkey(new BitmessageAddress("BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt"));
}
}
}