Migrated BitmessageContext and fixed some tests
This commit is contained in:
parent
fa0e53289c
commit
83e50e1ad1
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
451
core/src/main/java/ch/dissem/bitmessage/BitmessageContext.kt
Normal file
451
core/src/main/java/ch/dissem/bitmessage/BitmessageContext.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -131,7 +131,7 @@ 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))
|
||||
.expiresTime(now() + TTL)
|
||||
.stream(stream)
|
||||
.payload(payload)
|
||||
.build();
|
||||
|
Loading…
Reference in New Issue
Block a user