Migrated everything except
- the Bytes utilities class - it's easier to do in Java as with Kotlin byte + byte = int - the demo project, which I'm not sure I'll migrate. Maybe I'll make a new Kotlin Demo application
This commit is contained in:
498
core/src/main/kotlin/ch/dissem/bitmessage/BitmessageContext.kt
Normal file
498
core/src/main/kotlin/ch/dissem/bitmessage/BitmessageContext.kt
Normal file
@ -0,0 +1,498 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.CustomMessage
|
||||
import ch.dissem.bitmessage.entity.MessagePayload
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Status.DRAFT
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
||||
import ch.dissem.bitmessage.entity.payload.Broadcast
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.*
|
||||
import ch.dissem.bitmessage.utils.Property
|
||||
import ch.dissem.bitmessage.utils.UnixTime.HOUR
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
*
|
||||
* Use this class if you want to create a Bitmessage client.
|
||||
* You'll need the Builder to create a BitmessageContext, and set the following properties:
|
||||
*
|
||||
* * addressRepo
|
||||
* * inventory
|
||||
* * nodeRegistry
|
||||
* * networkHandler
|
||||
* * messageRepo
|
||||
* * streams
|
||||
*
|
||||
*
|
||||
* The default implementations in the different module builds can be used.
|
||||
*
|
||||
* The port defaults to 8444 (the default Bitmessage port)
|
||||
*/
|
||||
class BitmessageContext(
|
||||
cryptography: Cryptography,
|
||||
inventory: Inventory,
|
||||
nodeRegistry: NodeRegistry,
|
||||
networkHandler: NetworkHandler,
|
||||
addressRepository: AddressRepository,
|
||||
messageRepository: MessageRepository,
|
||||
proofOfWorkRepository: ProofOfWorkRepository,
|
||||
proofOfWorkEngine: ProofOfWorkEngine = MultiThreadedPOWEngine(),
|
||||
customCommandHandler: CustomCommandHandler = object : CustomCommandHandler {
|
||||
override fun handle(request: CustomMessage): MessagePayload? {
|
||||
BitmessageContext.LOG.debug("Received custom request, but no custom command handler configured.")
|
||||
return null
|
||||
}
|
||||
},
|
||||
listener: Listener,
|
||||
labeler: Labeler = DefaultLabeler(),
|
||||
port: Int = 8444,
|
||||
connectionTTL: Long = 30 * MINUTE,
|
||||
connectionLimit: Int = 150,
|
||||
sendPubkeyOnIdentityCreation: Boolean = true,
|
||||
doMissingProofOfWorkDelayInSeconds: Int = 30
|
||||
) {
|
||||
|
||||
private constructor(builder: BitmessageContext.Builder) : this(
|
||||
builder.cryptography,
|
||||
builder.inventory,
|
||||
builder.nodeRegistry,
|
||||
builder.networkHandler,
|
||||
builder.addressRepo,
|
||||
builder.messageRepo,
|
||||
builder.proofOfWorkRepository,
|
||||
builder.proofOfWorkEngine ?: MultiThreadedPOWEngine(),
|
||||
builder.customCommandHandler ?: object : CustomCommandHandler {
|
||||
override fun handle(request: CustomMessage): MessagePayload? {
|
||||
BitmessageContext.LOG.debug("Received custom request, but no custom command handler configured.")
|
||||
return null
|
||||
}
|
||||
},
|
||||
builder.listener,
|
||||
builder.labeler ?: DefaultLabeler(),
|
||||
builder.port,
|
||||
builder.connectionTTL,
|
||||
builder.connectionLimit,
|
||||
builder.sendPubkeyOnIdentityCreation,
|
||||
builder.doMissingProofOfWorkDelay
|
||||
)
|
||||
|
||||
private val sendPubkeyOnIdentityCreation: Boolean
|
||||
|
||||
/**
|
||||
* The [InternalContext] - normally you wouldn't need it,
|
||||
* unless you are doing something crazy with the protocol.
|
||||
*/
|
||||
val internals: InternalContext
|
||||
@JvmName("internals") get
|
||||
|
||||
val labeler: Labeler
|
||||
@JvmName("labeler") get
|
||||
|
||||
val addresses: AddressRepository
|
||||
@JvmName("addresses") get
|
||||
|
||||
val messages: MessageRepository
|
||||
@JvmName("messages") get
|
||||
|
||||
fun createIdentity(shorter: Boolean, vararg features: Feature): BitmessageAddress {
|
||||
val identity = BitmessageAddress(PrivateKey(
|
||||
shorter,
|
||||
internals.streams[0],
|
||||
NETWORK_NONCE_TRIALS_PER_BYTE,
|
||||
NETWORK_EXTRA_BYTES,
|
||||
*features
|
||||
))
|
||||
internals.addressRepository.save(identity)
|
||||
if (sendPubkeyOnIdentityCreation) {
|
||||
internals.sendPubkey(identity, identity.stream)
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
fun joinChan(passphrase: String, address: String): BitmessageAddress {
|
||||
val chan = BitmessageAddress.chan(address, passphrase)
|
||||
chan.alias = passphrase
|
||||
internals.addressRepository.save(chan)
|
||||
return chan
|
||||
}
|
||||
|
||||
fun createChan(passphrase: String): BitmessageAddress {
|
||||
// FIXME: hardcoded stream number
|
||||
val chan = BitmessageAddress.chan(1, passphrase)
|
||||
internals.addressRepository.save(chan)
|
||||
return chan
|
||||
}
|
||||
|
||||
fun createDeterministicAddresses(
|
||||
passphrase: String, numberOfAddresses: Int, version: Long, stream: Long, shorter: Boolean): List<BitmessageAddress> {
|
||||
val result = BitmessageAddress.deterministic(
|
||||
passphrase, numberOfAddresses, version, stream, shorter)
|
||||
for (i in result.indices) {
|
||||
val address = result[i]
|
||||
address.alias = "deterministic (" + (i + 1) + ")"
|
||||
internals.addressRepository.save(address)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun broadcast(from: BitmessageAddress, subject: String, message: String) {
|
||||
send(Plaintext(
|
||||
type = BROADCAST,
|
||||
from = from,
|
||||
subject = subject,
|
||||
body = message,
|
||||
status = DRAFT
|
||||
))
|
||||
}
|
||||
|
||||
fun send(from: BitmessageAddress, to: BitmessageAddress, subject: String, message: String) {
|
||||
if (from.privateKey == null) {
|
||||
throw IllegalArgumentException("'From' must be an identity, i.e. have a private key.")
|
||||
}
|
||||
send(Plaintext(
|
||||
type = MSG,
|
||||
from = from,
|
||||
to = to,
|
||||
subject = subject,
|
||||
body = message
|
||||
))
|
||||
}
|
||||
|
||||
fun send(msg: Plaintext) {
|
||||
if (msg.from.privateKey == null) {
|
||||
throw IllegalArgumentException("'From' must be an identity, i.e. have a private key.")
|
||||
}
|
||||
labeler.markAsSending(msg)
|
||||
val to = msg.to
|
||||
if (to != null) {
|
||||
if (to.pubkey == null) {
|
||||
LOG.info("Public key is missing from recipient. Requesting.")
|
||||
internals.requestPubkey(to)
|
||||
}
|
||||
if (to.pubkey == null) {
|
||||
internals.messageRepository.save(msg)
|
||||
}
|
||||
}
|
||||
if (to == null || to.pubkey != null) {
|
||||
LOG.info("Sending message.")
|
||||
internals.messageRepository.save(msg)
|
||||
if (msg.type == MSG) {
|
||||
internals.send(msg)
|
||||
} else {
|
||||
internals.send(
|
||||
msg.from,
|
||||
to,
|
||||
Factory.getBroadcast(msg),
|
||||
msg.ttl
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startup() {
|
||||
internals.networkHandler.start()
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
internals.networkHandler.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host a trusted node that must be reliable (it's used for every synchronization)
|
||||
* *
|
||||
* @param port of the trusted host, default is 8444
|
||||
* *
|
||||
* @param timeoutInSeconds synchronization should end no later than about 5 seconds after the timeout elapsed, even
|
||||
* * if not all objects were fetched
|
||||
* *
|
||||
* @param wait waits for the synchronization thread to finish
|
||||
*/
|
||||
fun synchronize(host: InetAddress, port: Int, timeoutInSeconds: Long, wait: Boolean) {
|
||||
val future = internals.networkHandler.synchronize(host, port, timeoutInSeconds)
|
||||
if (wait) {
|
||||
try {
|
||||
future.get()
|
||||
} catch (e: InterruptedException) {
|
||||
LOG.info("Thread was interrupted. Trying to shut down synchronization and returning.")
|
||||
future.cancel(true)
|
||||
} catch (e: CancellationException) {
|
||||
LOG.debug(e.message, e)
|
||||
} catch (e: ExecutionException) {
|
||||
LOG.debug(e.message, e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a custom message to a specific node (that should implement handling for this message type) and returns
|
||||
* the response, which in turn is expected to be a [CustomMessage].
|
||||
|
||||
* @param server the node's address
|
||||
* *
|
||||
* @param port the node's port
|
||||
* *
|
||||
* @param request the request
|
||||
* *
|
||||
* @return the response
|
||||
*/
|
||||
fun send(server: InetAddress, port: Int, request: CustomMessage): CustomMessage {
|
||||
return internals.networkHandler.send(server, port, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes expired objects from the inventory. You should call this method regularly,
|
||||
* e.g. daily and on each shutdown.
|
||||
*/
|
||||
fun cleanup() {
|
||||
internals.inventory.cleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends messages again whose time to live expired without being acknowledged. (And whose
|
||||
* recipient is expected to send acknowledgements.
|
||||
*
|
||||
*
|
||||
* You should call this method regularly, but be aware of the following:
|
||||
*
|
||||
* * As messages might be sent, POW will be done. It is therefore not advised to
|
||||
* call it on shutdown.
|
||||
* * It shouldn't be called right after startup, as it's possible the missing
|
||||
* acknowledgement was sent while the client was offline.
|
||||
* * Other than that, the call isn't expensive as long as there is no message
|
||||
* to send, so it might be a good idea to just call it every few minutes.
|
||||
*
|
||||
*/
|
||||
fun resendUnacknowledgedMessages() {
|
||||
internals.resendUnacknowledged()
|
||||
}
|
||||
|
||||
val isRunning: Boolean
|
||||
get() = internals.networkHandler.isRunning
|
||||
|
||||
fun addContact(contact: BitmessageAddress) {
|
||||
internals.addressRepository.save(contact)
|
||||
if (contact.pubkey == null) {
|
||||
// If it already existed, the saved contact might have the public key
|
||||
if (internals.addressRepository.getAddress(contact.address)!!.pubkey == null) {
|
||||
internals.requestPubkey(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addSubscribtion(address: BitmessageAddress) {
|
||||
address.isSubscribed = true
|
||||
internals.addressRepository.save(address)
|
||||
tryToFindBroadcastsForAddress(address)
|
||||
}
|
||||
|
||||
private fun tryToFindBroadcastsForAddress(address: BitmessageAddress) {
|
||||
for (objectMessage in internals.inventory.getObjects(address.stream, Broadcast.getVersion(address), ObjectType.BROADCAST)) {
|
||||
try {
|
||||
val broadcast = objectMessage.payload as Broadcast
|
||||
broadcast.decrypt(address)
|
||||
// This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with
|
||||
// other subscriptions and the interface stays as simple as possible.
|
||||
internals.networkListener.receive(objectMessage)
|
||||
} catch (ignore: DecryptionFailedException) {
|
||||
} catch (e: Exception) {
|
||||
LOG.debug(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun status(): Property {
|
||||
return Property("status",
|
||||
internals.networkHandler.getNetworkStatus(),
|
||||
Property("unacknowledged", internals.messageRepository.findMessagesToResend().size)
|
||||
)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun receive(plaintext: Plaintext)
|
||||
|
||||
/**
|
||||
* A message listener that needs a [BitmessageContext], i.e. for implementing some sort of chat bot.
|
||||
*/
|
||||
interface WithContext : Listener {
|
||||
fun setContext(ctx: BitmessageContext)
|
||||
}
|
||||
}
|
||||
|
||||
class Builder {
|
||||
internal var port = 8444
|
||||
internal var inventory by Delegates.notNull<Inventory>()
|
||||
internal var nodeRegistry by Delegates.notNull<NodeRegistry>()
|
||||
internal var networkHandler by Delegates.notNull<NetworkHandler>()
|
||||
internal var addressRepo by Delegates.notNull<AddressRepository>()
|
||||
internal var messageRepo by Delegates.notNull<MessageRepository>()
|
||||
internal var proofOfWorkRepository by Delegates.notNull<ProofOfWorkRepository>()
|
||||
internal var proofOfWorkEngine: ProofOfWorkEngine? = null
|
||||
internal var cryptography by Delegates.notNull<Cryptography>()
|
||||
internal var customCommandHandler: CustomCommandHandler? = null
|
||||
internal var labeler: Labeler? = null
|
||||
internal var listener by Delegates.notNull<Listener>()
|
||||
internal var connectionLimit = 150
|
||||
internal var connectionTTL = 30 * MINUTE
|
||||
internal var sendPubkeyOnIdentityCreation = true
|
||||
internal var doMissingProofOfWorkDelay = 30
|
||||
|
||||
fun port(port: Int): Builder {
|
||||
this.port = port
|
||||
return this
|
||||
}
|
||||
|
||||
fun inventory(inventory: Inventory): Builder {
|
||||
this.inventory = inventory
|
||||
return this
|
||||
}
|
||||
|
||||
fun nodeRegistry(nodeRegistry: NodeRegistry): Builder {
|
||||
this.nodeRegistry = nodeRegistry
|
||||
return this
|
||||
}
|
||||
|
||||
fun networkHandler(networkHandler: NetworkHandler): Builder {
|
||||
this.networkHandler = networkHandler
|
||||
return this
|
||||
}
|
||||
|
||||
fun addressRepo(addressRepo: AddressRepository): Builder {
|
||||
this.addressRepo = addressRepo
|
||||
return this
|
||||
}
|
||||
|
||||
fun messageRepo(messageRepo: MessageRepository): Builder {
|
||||
this.messageRepo = messageRepo
|
||||
return this
|
||||
}
|
||||
|
||||
fun powRepo(proofOfWorkRepository: ProofOfWorkRepository): Builder {
|
||||
this.proofOfWorkRepository = proofOfWorkRepository
|
||||
return this
|
||||
}
|
||||
|
||||
fun cryptography(cryptography: Cryptography): Builder {
|
||||
this.cryptography = cryptography
|
||||
return this
|
||||
}
|
||||
|
||||
fun customCommandHandler(handler: CustomCommandHandler): Builder {
|
||||
this.customCommandHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
fun proofOfWorkEngine(proofOfWorkEngine: ProofOfWorkEngine): Builder {
|
||||
this.proofOfWorkEngine = proofOfWorkEngine
|
||||
return this
|
||||
}
|
||||
|
||||
fun labeler(labeler: Labeler): Builder {
|
||||
this.labeler = labeler
|
||||
return this
|
||||
}
|
||||
|
||||
fun listener(listener: Listener): Builder {
|
||||
this.listener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
@JvmName("kotlinListener")
|
||||
fun listener(listener: (Plaintext) -> Unit): Builder {
|
||||
this.listener = object : Listener {
|
||||
override fun receive(plaintext: Plaintext) {
|
||||
listener.invoke(plaintext)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun connectionLimit(connectionLimit: Int): Builder {
|
||||
this.connectionLimit = connectionLimit
|
||||
return this
|
||||
}
|
||||
|
||||
fun connectionTTL(hours: Int): Builder {
|
||||
this.connectionTTL = hours * HOUR
|
||||
return this
|
||||
}
|
||||
|
||||
fun doMissingProofOfWorkDelay(seconds: Int) {
|
||||
this.doMissingProofOfWorkDelay = seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* By default a client will send the public key when an identity is being created. On weaker devices
|
||||
* this behaviour might not be desirable.
|
||||
*/
|
||||
fun doNotSendPubkeyOnIdentityCreation(): Builder {
|
||||
this.sendPubkeyOnIdentityCreation = false
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): BitmessageContext {
|
||||
return BitmessageContext(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
this.labeler = labeler
|
||||
this.internals = InternalContext(
|
||||
cryptography,
|
||||
inventory,
|
||||
nodeRegistry,
|
||||
networkHandler,
|
||||
addressRepository,
|
||||
messageRepository,
|
||||
proofOfWorkRepository,
|
||||
proofOfWorkEngine,
|
||||
customCommandHandler,
|
||||
listener,
|
||||
labeler,
|
||||
port,
|
||||
connectionTTL,
|
||||
connectionLimit
|
||||
)
|
||||
this.addresses = addressRepository
|
||||
this.messages = messageRepository
|
||||
this.sendPubkeyOnIdentityCreation = sendPubkeyOnIdentityCreation
|
||||
(listener as? Listener.WithContext)?.setContext(this)
|
||||
internals.proofOfWorkService.doMissingProofOfWork(doMissingProofOfWorkDelayInSeconds * 1000L)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField val CURRENT_VERSION = 3
|
||||
private val LOG = LoggerFactory.getLogger(BitmessageContext::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED
|
||||
import ch.dissem.bitmessage.entity.payload.*
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.ports.Labeler
|
||||
import ch.dissem.bitmessage.ports.NetworkHandler
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
internal open class DefaultMessageListener(
|
||||
private val labeler: Labeler,
|
||||
private val listener: BitmessageContext.Listener
|
||||
) : NetworkHandler.MessageListener, InternalContext.ContextHolder {
|
||||
private lateinit var ctx: InternalContext
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
ctx = context
|
||||
}
|
||||
|
||||
override fun receive(objectMessage: ObjectMessage) {
|
||||
val payload = objectMessage.payload
|
||||
|
||||
when (payload.type) {
|
||||
ObjectType.GET_PUBKEY -> {
|
||||
receive(objectMessage, payload as GetPubkey)
|
||||
}
|
||||
ObjectType.PUBKEY -> {
|
||||
receive(payload as Pubkey)
|
||||
}
|
||||
ObjectType.MSG -> {
|
||||
receive(objectMessage, payload as Msg)
|
||||
}
|
||||
ObjectType.BROADCAST -> {
|
||||
receive(objectMessage, payload as Broadcast)
|
||||
}
|
||||
null -> {
|
||||
if (payload is GenericPayload) {
|
||||
receive(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun receive(objectMessage: ObjectMessage, getPubkey: GetPubkey) {
|
||||
val identity = ctx.addressRepository.findIdentity(getPubkey.ripeTag)
|
||||
if (identity != null && identity.privateKey != null && !identity.isChan) {
|
||||
LOG.info("Got pubkey request for identity " + identity)
|
||||
// FIXME: only send pubkey if it wasn't sent in the last TTL.pubkey() days
|
||||
ctx.sendPubkey(identity, objectMessage.stream)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun receive(pubkey: Pubkey) {
|
||||
try {
|
||||
if (pubkey is V4Pubkey) {
|
||||
ctx.addressRepository.findContact(pubkey.tag)?.let {
|
||||
if (it.pubkey == null) {
|
||||
pubkey.decrypt(it.publicDecryptionKey)
|
||||
updatePubkey(it, pubkey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.addressRepository.findContact(pubkey.ripe)?.let {
|
||||
if (it.pubkey == null) {
|
||||
updatePubkey(it, pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: DecryptionFailedException) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updatePubkey(address: BitmessageAddress, pubkey: Pubkey) {
|
||||
address.pubkey = pubkey
|
||||
LOG.info("Got pubkey for contact " + address)
|
||||
ctx.addressRepository.save(address)
|
||||
val messages = ctx.messageRepository.findMessages(PUBKEY_REQUESTED, address)
|
||||
LOG.info("Sending " + messages.size + " messages for contact " + address)
|
||||
for (msg in messages) {
|
||||
ctx.labeler.markAsSending(msg)
|
||||
ctx.messageRepository.save(msg)
|
||||
ctx.send(msg)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun receive(objectMessage: ObjectMessage, msg: Msg) {
|
||||
for (identity in ctx.addressRepository.getIdentities()) {
|
||||
try {
|
||||
msg.decrypt(identity.privateKey!!.privateEncryptionKey)
|
||||
val plaintext = msg.plaintext!!
|
||||
plaintext.to = identity
|
||||
if (!objectMessage.isSignatureValid(plaintext.from.pubkey!!)) {
|
||||
LOG.warn("Msg with IV " + objectMessage.inventoryVector + " was successfully decrypted, but signature check failed. Ignoring.")
|
||||
} else {
|
||||
receive(objectMessage.inventoryVector, plaintext)
|
||||
}
|
||||
break
|
||||
} catch (_: DecryptionFailedException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun receive(ack: GenericPayload) {
|
||||
if (ack.data.size == Msg.ACK_LENGTH) {
|
||||
ctx.messageRepository.getMessageForAck(ack.data)?.let {
|
||||
ctx.labeler.markAsAcknowledged(it)
|
||||
ctx.messageRepository.save(it)
|
||||
} ?: LOG.debug("Message not found for ack ${hex(ack.data)}")
|
||||
}
|
||||
}
|
||||
|
||||
protected fun receive(objectMessage: ObjectMessage, broadcast: Broadcast) {
|
||||
val tag = (broadcast as? V5Broadcast)?.tag
|
||||
ctx.addressRepository.getSubscriptions(broadcast.version)
|
||||
.filter { tag == null || Arrays.equals(tag, it.tag) }
|
||||
.forEach {
|
||||
try {
|
||||
broadcast.decrypt(it.publicDecryptionKey)
|
||||
if (!objectMessage.isSignatureValid(broadcast.plaintext!!.from.pubkey!!)) {
|
||||
LOG.warn("Broadcast with IV " + objectMessage.inventoryVector + " was successfully decrypted, but signature check failed. Ignoring.")
|
||||
} else {
|
||||
receive(objectMessage.inventoryVector, broadcast.plaintext!!)
|
||||
}
|
||||
} catch (_: DecryptionFailedException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun receive(iv: InventoryVector, msg: Plaintext) {
|
||||
val contact = ctx.addressRepository.getAddress(msg.from.address)
|
||||
if (contact != null && contact.pubkey == null) {
|
||||
updatePubkey(contact, msg.from.pubkey!!)
|
||||
}
|
||||
|
||||
msg.inventoryVector = iv
|
||||
labeler.setLabels(msg)
|
||||
ctx.messageRepository.save(msg)
|
||||
listener.receive(msg)
|
||||
|
||||
if (msg.type == Plaintext.Type.MSG && msg.to!!.has(Pubkey.Feature.DOES_ACK)) {
|
||||
msg.ackMessage?.let {
|
||||
ctx.inventory.storeObject(it)
|
||||
ctx.networkHandler.offer(it.inventoryVector)
|
||||
} ?: LOG.debug("ack message expected")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(DefaultMessageListener::class.java)
|
||||
}
|
||||
}
|
224
core/src/main/kotlin/ch/dissem/bitmessage/InternalContext.kt
Normal file
224
core/src/main/kotlin/ch/dissem/bitmessage/InternalContext.kt
Normal file
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Encrypted
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.payload.*
|
||||
import ch.dissem.bitmessage.ports.*
|
||||
import ch.dissem.bitmessage.utils.Singleton
|
||||
import ch.dissem.bitmessage.utils.TTL
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* The internal context should normally only be used for port implementations. If you need it in your client
|
||||
* implementation, you're either doing something wrong, something very weird, or the BitmessageContext should
|
||||
* get extended.
|
||||
*
|
||||
*
|
||||
* On the other hand, if you need the BitmessageContext in a port implementation, the same thing might apply.
|
||||
*
|
||||
*/
|
||||
class InternalContext(
|
||||
val cryptography: Cryptography,
|
||||
val inventory: ch.dissem.bitmessage.ports.Inventory,
|
||||
val nodeRegistry: NodeRegistry,
|
||||
val networkHandler: NetworkHandler,
|
||||
val addressRepository: AddressRepository,
|
||||
val messageRepository: ch.dissem.bitmessage.ports.MessageRepository,
|
||||
val proofOfWorkRepository: ProofOfWorkRepository,
|
||||
val proofOfWorkEngine: ProofOfWorkEngine,
|
||||
val customCommandHandler: CustomCommandHandler,
|
||||
listener: BitmessageContext.Listener,
|
||||
val labeler: Labeler,
|
||||
|
||||
val port: Int,
|
||||
val connectionTTL: Long,
|
||||
val connectionLimit: Int
|
||||
) {
|
||||
|
||||
private val threadPool = Executors.newCachedThreadPool()
|
||||
|
||||
val proofOfWorkService: ProofOfWorkService = ProofOfWorkService()
|
||||
val networkListener: NetworkHandler.MessageListener = DefaultMessageListener(labeler, listener)
|
||||
val clientNonce: Long = cryptography.randomNonce()
|
||||
private val _streams = TreeSet<Long>()
|
||||
val streams: LongArray
|
||||
get() = _streams.toLongArray()
|
||||
|
||||
init {
|
||||
Singleton.initialize(cryptography)
|
||||
|
||||
// TODO: streams of new identities and subscriptions should also be added. This works only after a restart.
|
||||
addressRepository.getIdentities().mapTo(_streams) { it.stream }
|
||||
addressRepository.getSubscriptions().mapTo(_streams) { it.stream }
|
||||
if (_streams.isEmpty()) {
|
||||
_streams.add(1L)
|
||||
}
|
||||
|
||||
init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
|
||||
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, customCommandHandler, labeler,
|
||||
networkListener)
|
||||
}
|
||||
|
||||
private fun init(vararg objects: Any) {
|
||||
objects.filter { it is ContextHolder }.forEach { (it as ContextHolder).setContext(this) }
|
||||
}
|
||||
|
||||
fun send(plaintext: Plaintext) {
|
||||
if (plaintext.ackMessage != null) {
|
||||
val expires = UnixTime.now + plaintext.ttl
|
||||
LOG.info("Expires at " + expires)
|
||||
proofOfWorkService.doProofOfWorkWithAck(plaintext, expires)
|
||||
} else {
|
||||
send(plaintext.from, plaintext.to, Msg(plaintext), plaintext.ttl)
|
||||
}
|
||||
}
|
||||
|
||||
fun send(from: BitmessageAddress, to: BitmessageAddress?, payload: ObjectPayload,
|
||||
timeToLive: Long) {
|
||||
val recipient = to ?: from
|
||||
val expires = UnixTime.now + timeToLive
|
||||
LOG.info("Expires at " + expires)
|
||||
val objectMessage = ObjectMessage(
|
||||
stream = recipient.stream,
|
||||
expiresTime = expires,
|
||||
payload = payload
|
||||
)
|
||||
if (objectMessage.isSigned) {
|
||||
objectMessage.sign(
|
||||
from.privateKey ?: throw IllegalArgumentException("The given sending address is no identity")
|
||||
)
|
||||
}
|
||||
if (payload is Broadcast) {
|
||||
payload.encrypt()
|
||||
} else if (payload is Encrypted) {
|
||||
objectMessage.encrypt(
|
||||
recipient.pubkey ?: throw IllegalArgumentException("The public key for the recipient isn't available")
|
||||
)
|
||||
}
|
||||
proofOfWorkService.doProofOfWork(to, objectMessage)
|
||||
}
|
||||
|
||||
fun sendPubkey(identity: BitmessageAddress, targetStream: Long) {
|
||||
val expires = UnixTime.now + TTL.pubkey
|
||||
LOG.info("Expires at " + expires)
|
||||
val payload = identity.pubkey ?: throw IllegalArgumentException("The given address is no identity")
|
||||
val response = ObjectMessage(
|
||||
expiresTime = expires,
|
||||
stream = targetStream,
|
||||
payload = payload
|
||||
)
|
||||
response.sign(
|
||||
identity.privateKey ?: throw IllegalArgumentException("The given address is no identity")
|
||||
)
|
||||
response.encrypt(cryptography.createPublicKey(identity.publicDecryptionKey))
|
||||
// TODO: remember that the pubkey is just about to be sent, and on which stream!
|
||||
proofOfWorkService.doProofOfWork(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Be aware that if the pubkey already exists in the inventory, the metods will not request it and the callback
|
||||
* for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB.
|
||||
*/
|
||||
fun requestPubkey(contact: BitmessageAddress) {
|
||||
threadPool.execute {
|
||||
val stored = addressRepository.getAddress(contact.address)
|
||||
|
||||
tryToFindMatchingPubkey(contact)
|
||||
if (contact.pubkey != null) {
|
||||
if (stored != null) {
|
||||
stored.pubkey = contact.pubkey
|
||||
addressRepository.save(stored)
|
||||
} else {
|
||||
addressRepository.save(contact)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
if (stored == null) {
|
||||
addressRepository.save(contact)
|
||||
}
|
||||
|
||||
val expires = UnixTime.now + TTL.getpubkey
|
||||
LOG.info("Expires at $expires")
|
||||
val request = ObjectMessage(
|
||||
stream = contact.stream,
|
||||
expiresTime = expires,
|
||||
payload = GetPubkey(contact)
|
||||
)
|
||||
proofOfWorkService.doProofOfWork(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryToFindMatchingPubkey(address: BitmessageAddress) {
|
||||
addressRepository.getAddress(address.address)?.let {
|
||||
address.alias = it.alias
|
||||
address.isSubscribed = it.isSubscribed
|
||||
}
|
||||
for (objectMessage in inventory.getObjects(address.stream, address.version, ObjectType.PUBKEY)) {
|
||||
try {
|
||||
val pubkey = objectMessage.payload as Pubkey
|
||||
if (address.version == 4L) {
|
||||
val v4Pubkey = pubkey as V4Pubkey
|
||||
if (Arrays.equals(address.tag, v4Pubkey.tag)) {
|
||||
v4Pubkey.decrypt(address.publicDecryptionKey)
|
||||
if (objectMessage.isSignatureValid(v4Pubkey)) {
|
||||
address.pubkey = v4Pubkey
|
||||
addressRepository.save(address)
|
||||
break
|
||||
} else {
|
||||
LOG.info("Found pubkey for $address but signature is invalid")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Arrays.equals(pubkey.ripe, address.ripe)) {
|
||||
address.pubkey = pubkey
|
||||
addressRepository.save(address)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.debug(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resendUnacknowledged() {
|
||||
val messages = messageRepository.findMessagesToResend()
|
||||
for (message in messages) {
|
||||
send(message)
|
||||
messageRepository.save(message)
|
||||
}
|
||||
}
|
||||
|
||||
interface ContextHolder {
|
||||
fun setContext(context: InternalContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(InternalContext::class.java)
|
||||
|
||||
@JvmField val NETWORK_NONCE_TRIALS_PER_BYTE: Long = 1000
|
||||
@JvmField val NETWORK_EXTRA_BYTES: Long = 1000
|
||||
}
|
||||
}
|
121
core/src/main/kotlin/ch/dissem/bitmessage/ProofOfWorkService.kt
Normal file
121
core/src/main/kotlin/ch/dissem/bitmessage/ProofOfWorkService.kt
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
import ch.dissem.bitmessage.entity.*
|
||||
import ch.dissem.bitmessage.entity.payload.Msg
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ProofOfWorkService : ProofOfWorkEngine.Callback, InternalContext.ContextHolder {
|
||||
|
||||
private lateinit var ctx: InternalContext
|
||||
private val cryptography by lazy { ctx.cryptography }
|
||||
private val powRepo by lazy { ctx.proofOfWorkRepository }
|
||||
private val messageRepo by lazy { ctx.messageRepository }
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
ctx = context
|
||||
}
|
||||
|
||||
fun doMissingProofOfWork(delayInMilliseconds: Long) {
|
||||
val items = powRepo.getItems()
|
||||
if (items.isEmpty()) return
|
||||
|
||||
// Wait for 30 seconds, to let the application start up before putting heavy load on the CPU
|
||||
Timer().schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
LOG.info("Doing POW for " + items.size + " tasks.")
|
||||
for (initialHash in items) {
|
||||
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
|
||||
cryptography.doProofOfWork(objectMessage, nonceTrialsPerByte, extraBytes,
|
||||
this@ProofOfWorkService)
|
||||
}
|
||||
}
|
||||
}, delayInMilliseconds)
|
||||
}
|
||||
|
||||
fun doProofOfWork(objectMessage: ObjectMessage) {
|
||||
doProofOfWork(null, objectMessage)
|
||||
}
|
||||
|
||||
fun doProofOfWork(recipient: BitmessageAddress?, objectMessage: ObjectMessage) {
|
||||
val pubkey = recipient?.pubkey
|
||||
|
||||
val nonceTrialsPerByte = pubkey?.nonceTrialsPerByte ?: NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
val extraBytes = pubkey?.extraBytes ?: NETWORK_EXTRA_BYTES
|
||||
|
||||
powRepo.putObject(objectMessage, nonceTrialsPerByte, extraBytes)
|
||||
if (objectMessage.payload is PlaintextHolder) {
|
||||
objectMessage.payload.plaintext?.let {
|
||||
it.initialHash = cryptography.getInitialHash(objectMessage)
|
||||
messageRepo.save(it)
|
||||
} ?: LOG.error("PlaintextHolder without Plaintext shouldn't make it to the POW")
|
||||
}
|
||||
cryptography.doProofOfWork(objectMessage, nonceTrialsPerByte, extraBytes, this)
|
||||
}
|
||||
|
||||
fun doProofOfWorkWithAck(plaintext: Plaintext, expirationTime: Long) {
|
||||
val ack = plaintext.ackMessage!!
|
||||
messageRepo.save(plaintext)
|
||||
val item = Item(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES,
|
||||
expirationTime, plaintext)
|
||||
powRepo.putObject(item)
|
||||
cryptography.doProofOfWork(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, this)
|
||||
}
|
||||
|
||||
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
|
||||
val (objectMessage, _, _, expirationTime, message) = powRepo.getItem(initialHash)
|
||||
if (message == null) {
|
||||
objectMessage.nonce = nonce
|
||||
messageRepo.getMessage(initialHash)?.let {
|
||||
it.inventoryVector = objectMessage.inventoryVector
|
||||
it.updateNextTry()
|
||||
ctx.labeler.markAsSent(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
ctx.inventory.storeObject(objectMessage)
|
||||
ctx.networkHandler.offer(objectMessage.inventoryVector)
|
||||
} else {
|
||||
message.ackMessage!!.nonce = nonce
|
||||
val newObjectMessage = ObjectMessage.Builder()
|
||||
.stream(message.stream)
|
||||
.expiresTime(expirationTime!!)
|
||||
.payload(Msg(message))
|
||||
.build()
|
||||
if (newObjectMessage.isSigned) {
|
||||
newObjectMessage.sign(message.from.privateKey!!)
|
||||
}
|
||||
if (newObjectMessage.payload is Encrypted) {
|
||||
newObjectMessage.encrypt(message.to!!.pubkey!!)
|
||||
}
|
||||
doProofOfWork(message.to, newObjectMessage)
|
||||
}
|
||||
powRepo.removeObject(initialHash)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(ProofOfWorkService::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.constants
|
||||
|
||||
/**
|
||||
* Some network constants
|
||||
*/
|
||||
object Network {
|
||||
@JvmField val NETWORK_MAGIC_NUMBER = 8
|
||||
@JvmField val HEADER_SIZE = 24
|
||||
@JvmField val MAX_PAYLOAD_SIZE = 1600003
|
||||
@JvmField val MAX_MESSAGE_SIZE = HEADER_SIZE + MAX_PAYLOAD_SIZE
|
||||
}
|
43
core/src/main/kotlin/ch/dissem/bitmessage/entity/Addr.kt
Normal file
43
core/src/main/kotlin/ch/dissem/bitmessage/entity/Addr.kt
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* The 'addr' command holds a list of known active Bitmessage nodes.
|
||||
*/
|
||||
data class Addr constructor(val addresses: List<NetworkAddress>) : MessagePayload {
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.ADDR
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
Encode.varInt(addresses.size, out)
|
||||
for (address in addresses) {
|
||||
address.write(out)
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
Encode.varInt(addresses.size, buffer)
|
||||
for (address in addresses) {
|
||||
address.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.utils.AccessCounter
|
||||
import ch.dissem.bitmessage.utils.Base58
|
||||
import ch.dissem.bitmessage.utils.Bytes
|
||||
import ch.dissem.bitmessage.utils.Decode.bytes
|
||||
import ch.dissem.bitmessage.utils.Decode.varInt
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address
|
||||
* holding private keys.
|
||||
*/
|
||||
class BitmessageAddress : Serializable {
|
||||
|
||||
val version: Long
|
||||
val stream: Long
|
||||
val ripe: ByteArray
|
||||
val tag: ByteArray?
|
||||
/**
|
||||
* The private key used to decrypt Pubkey objects (for v4 addresses) and broadcasts. It's easier to just create
|
||||
* it regardless of address version.
|
||||
*/
|
||||
val publicDecryptionKey: ByteArray
|
||||
|
||||
val address: String
|
||||
|
||||
var privateKey: PrivateKey? = null
|
||||
private set
|
||||
var pubkey: Pubkey? = null
|
||||
set(pubkey) {
|
||||
if (pubkey != null) {
|
||||
if (pubkey is V4Pubkey) {
|
||||
if (!Arrays.equals(tag, pubkey.tag))
|
||||
throw IllegalArgumentException("Pubkey has incompatible tag")
|
||||
}
|
||||
if (!Arrays.equals(ripe, pubkey.ripe))
|
||||
throw IllegalArgumentException("Pubkey has incompatible ripe")
|
||||
field = pubkey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var alias: String? = null
|
||||
var isSubscribed: Boolean = false
|
||||
var isChan: Boolean = false
|
||||
|
||||
internal constructor(version: Long, stream: Long, ripe: ByteArray) {
|
||||
this.version = version
|
||||
this.stream = stream
|
||||
this.ripe = ripe
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
Encode.varInt(version, os)
|
||||
Encode.varInt(stream, os)
|
||||
if (version < 4) {
|
||||
val checksum = cryptography().sha512(os.toByteArray(), ripe)
|
||||
this.tag = null
|
||||
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32)
|
||||
} else {
|
||||
// for tag and decryption key, the checksum has to be created with 0x00 padding
|
||||
val checksum = cryptography().doubleSha512(os.toByteArray(), ripe)
|
||||
this.tag = Arrays.copyOfRange(checksum, 32, 64)
|
||||
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32)
|
||||
}
|
||||
// but for the address and its checksum they need to be stripped
|
||||
val offset = Bytes.numberOfLeadingZeros(ripe)
|
||||
os.write(ripe, offset, ripe.size - offset)
|
||||
val checksum = cryptography().doubleSha512(os.toByteArray())
|
||||
os.write(checksum, 0, 4)
|
||||
this.address = "BM-" + Base58.encode(os.toByteArray())
|
||||
}
|
||||
|
||||
constructor(publicKey: Pubkey) : this(publicKey.version, publicKey.stream, publicKey.ripe) {
|
||||
this.pubkey = publicKey
|
||||
}
|
||||
|
||||
constructor(address: String, passphrase: String) : this(address) {
|
||||
val key = PrivateKey(this, passphrase)
|
||||
if (!Arrays.equals(ripe, key.pubkey.ripe)) {
|
||||
throw IllegalArgumentException("Wrong address or passphrase")
|
||||
}
|
||||
this.privateKey = key
|
||||
this.pubkey = key.pubkey
|
||||
}
|
||||
|
||||
constructor(privateKey: PrivateKey) : this(privateKey.pubkey) {
|
||||
this.privateKey = privateKey
|
||||
}
|
||||
|
||||
constructor(address: String) {
|
||||
this.address = address
|
||||
val bytes = Base58.decode(address.substring(3))
|
||||
val `in` = ByteArrayInputStream(bytes)
|
||||
val counter = AccessCounter()
|
||||
this.version = varInt(`in`, counter)
|
||||
this.stream = varInt(`in`, counter)
|
||||
this.ripe = Bytes.expand(bytes(`in`, bytes.size - counter.length() - 4), 20)
|
||||
|
||||
// test checksum
|
||||
var checksum = cryptography().doubleSha512(bytes, bytes.size - 4)
|
||||
val expectedChecksum = bytes(`in`, 4)
|
||||
for (i in 0..3) {
|
||||
if (expectedChecksum[i] != checksum[i])
|
||||
throw IllegalArgumentException("Checksum of address failed")
|
||||
}
|
||||
if (version < 4) {
|
||||
checksum = cryptography().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe)
|
||||
this.tag = null
|
||||
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32)
|
||||
} else {
|
||||
checksum = cryptography().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe)
|
||||
this.tag = Arrays.copyOfRange(checksum, 32, 64)
|
||||
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return alias ?: address
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is BitmessageAddress) return false
|
||||
return version == other.version &&
|
||||
stream == other.stream &&
|
||||
Arrays.equals(ripe, other.ripe)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Arrays.hashCode(ripe)
|
||||
}
|
||||
|
||||
fun has(feature: Feature?): Boolean {
|
||||
return feature?.isActive(pubkey?.behaviorBitfield ?: 0) ?: false
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun chan(address: String, passphrase: String): BitmessageAddress {
|
||||
val result = BitmessageAddress(address, passphrase)
|
||||
result.isChan = true
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun chan(stream: Long, passphrase: String): BitmessageAddress {
|
||||
val privateKey = PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase)
|
||||
val result = BitmessageAddress(privateKey)
|
||||
result.isChan = true
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun deterministic(passphrase: String, numberOfAddresses: Int,
|
||||
version: Long, stream: Long, shorter: Boolean): List<BitmessageAddress> {
|
||||
val result = ArrayList<BitmessageAddress>(numberOfAddresses)
|
||||
val privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter)
|
||||
for (pk in privateKeys) {
|
||||
result.add(BitmessageAddress(pk))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun calculateTag(version: Long, stream: Long, ripe: ByteArray): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
Encode.varInt(version, out)
|
||||
Encode.varInt(stream, out)
|
||||
out.write(ripe)
|
||||
return Arrays.copyOfRange(cryptography().doubleSha512(out.toByteArray()), 32, 64)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.utils.AccessCounter
|
||||
import ch.dissem.bitmessage.utils.Decode.bytes
|
||||
import ch.dissem.bitmessage.utils.Decode.varString
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
open class CustomMessage(val customCommand: String, private val data: ByteArray? = null) : MessagePayload {
|
||||
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.CUSTOM
|
||||
|
||||
val isError: Boolean
|
||||
|
||||
fun getData(): ByteArray {
|
||||
if (data != null) {
|
||||
return data
|
||||
} else {
|
||||
val out = ByteArrayOutputStream()
|
||||
write(out)
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
if (data != null) {
|
||||
Encode.varString(customCommand, out)
|
||||
out.write(data)
|
||||
} else {
|
||||
throw ApplicationException("Tried to write custom message without data. "
|
||||
+ "Programmer: did you forget to override #write()?")
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
if (data != null) {
|
||||
Encode.varString(customCommand, buffer)
|
||||
buffer.put(data)
|
||||
} else {
|
||||
throw ApplicationException("Tried to write custom message without data. "
|
||||
+ "Programmer: did you forget to override #write()?")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val COMMAND_ERROR = "ERROR"
|
||||
|
||||
fun read(`in`: InputStream, length: Int): CustomMessage {
|
||||
val counter = AccessCounter()
|
||||
return CustomMessage(varString(`in`, counter), bytes(`in`, length - counter.length()))
|
||||
}
|
||||
|
||||
fun error(message: String): CustomMessage {
|
||||
return CustomMessage(COMMAND_ERROR, message.toByteArray(charset("UTF-8")))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
this.isError = COMMAND_ERROR == customCommand
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
|
||||
/**
|
||||
* Used for objects that have encrypted content
|
||||
*/
|
||||
interface Encrypted {
|
||||
fun encrypt(publicKey: ByteArray)
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
fun decrypt(privateKey: ByteArray)
|
||||
|
||||
val isDecrypted: Boolean
|
||||
}
|
48
core/src/main/kotlin/ch/dissem/bitmessage/entity/GetData.kt
Normal file
48
core/src/main/kotlin/ch/dissem/bitmessage/entity/GetData.kt
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* The 'getdata' command is used to request objects from a node.
|
||||
*/
|
||||
class GetData constructor(var inventory: List<InventoryVector>) : MessagePayload {
|
||||
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.GETDATA
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
Encode.varInt(inventory.size, out)
|
||||
for (iv in inventory) {
|
||||
iv.write(out)
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
Encode.varInt(inventory.size, buffer)
|
||||
for (iv in inventory) {
|
||||
iv.write(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField val MAX_INVENTORY_SIZE = 50000
|
||||
}
|
||||
}
|
44
core/src/main/kotlin/ch/dissem/bitmessage/entity/Inv.kt
Normal file
44
core/src/main/kotlin/ch/dissem/bitmessage/entity/Inv.kt
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items.
|
||||
*/
|
||||
class Inv constructor(val inventory: List<InventoryVector>) : MessagePayload {
|
||||
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.INV
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
Encode.varInt(inventory.size, out)
|
||||
for (iv in inventory) {
|
||||
iv.write(out)
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
Encode.varInt(inventory.size, buffer)
|
||||
for (iv in inventory) {
|
||||
iv.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
/**
|
||||
* A command can hold a network message payload
|
||||
*/
|
||||
interface MessagePayload : Streamable {
|
||||
val command: Command
|
||||
|
||||
enum class Command {
|
||||
VERSION, VERACK, ADDR, INV, GETDATA, OBJECT, CUSTOM
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* A network message is exchanged between two nodes.
|
||||
*/
|
||||
data class NetworkMessage(
|
||||
/**
|
||||
* The actual data, a message or an object. Not to be confused with objectPayload.
|
||||
*/
|
||||
val payload: MessagePayload
|
||||
) : Streamable {
|
||||
|
||||
/**
|
||||
* First 4 bytes of sha512(payload)
|
||||
*/
|
||||
private fun getChecksum(bytes: ByteArray): ByteArray {
|
||||
val d = cryptography().sha512(bytes)
|
||||
return byteArrayOf(d[0], d[1], d[2], d[3])
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: OutputStream) {
|
||||
// magic
|
||||
Encode.int32(MAGIC, out)
|
||||
|
||||
// ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected)
|
||||
val command = payload.command.name.toLowerCase()
|
||||
out.write(command.toByteArray(charset("ASCII")))
|
||||
for (i in command.length..11) {
|
||||
out.write(0x0)
|
||||
}
|
||||
|
||||
val payloadBytes = Encode.bytes(payload)
|
||||
|
||||
// Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would
|
||||
// ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are
|
||||
// larger than this.
|
||||
Encode.int32(payloadBytes.size, out)
|
||||
|
||||
// checksum
|
||||
out.write(getChecksum(payloadBytes))
|
||||
|
||||
// message payload
|
||||
out.write(payloadBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* A more efficient implementation of the write method, writing header data to the provided buffer and returning
|
||||
* a new buffer containing the payload.
|
||||
|
||||
* @param headerBuffer where the header data is written to (24 bytes)
|
||||
* *
|
||||
* @return a buffer containing the payload, ready to be read.
|
||||
*/
|
||||
fun writeHeaderAndGetPayloadBuffer(headerBuffer: ByteBuffer): ByteBuffer {
|
||||
return ByteBuffer.wrap(writeHeader(headerBuffer))
|
||||
}
|
||||
|
||||
/**
|
||||
* For improved memory efficiency, you should use [.writeHeaderAndGetPayloadBuffer]
|
||||
* and write the header buffer as well as the returned payload buffer into the channel.
|
||||
|
||||
* @param buffer where everything gets written to. Needs to be large enough for the whole message
|
||||
* * to be written.
|
||||
*/
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
val payloadBytes = writeHeader(buffer)
|
||||
buffer.put(payloadBytes)
|
||||
}
|
||||
|
||||
private fun writeHeader(out: ByteBuffer): ByteArray {
|
||||
// magic
|
||||
Encode.int32(MAGIC, out)
|
||||
|
||||
// ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected)
|
||||
val command = payload.command.name.toLowerCase()
|
||||
out.put(command.toByteArray(charset("ASCII")))
|
||||
|
||||
for (i in command.length..11) {
|
||||
out.put(0.toByte())
|
||||
}
|
||||
|
||||
val payloadBytes = Encode.bytes(payload)
|
||||
|
||||
// Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would
|
||||
// ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are
|
||||
// larger than this.
|
||||
Encode.int32(payloadBytes.size, out)
|
||||
|
||||
// checksum
|
||||
out.put(getChecksum(payloadBytes))
|
||||
|
||||
// message payload
|
||||
return payloadBytes
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Magic value indicating message origin network, and used to seek to next message when stream state is unknown
|
||||
*/
|
||||
val MAGIC = 0xE9BEB4D9.toInt()
|
||||
val MAGIC_BYTES = ByteBuffer.allocate(4).putInt(MAGIC).array()
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectPayload
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.utils.Bytes
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The 'object' command sends an object that is shared throughout the network.
|
||||
*/
|
||||
data class ObjectMessage(
|
||||
var nonce: ByteArray? = null,
|
||||
val expiresTime: Long,
|
||||
val payload: ObjectPayload,
|
||||
val type: Long,
|
||||
/**
|
||||
* The object's version
|
||||
*/
|
||||
val version: Long,
|
||||
val stream: Long
|
||||
) : MessagePayload {
|
||||
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.OBJECT
|
||||
|
||||
constructor(
|
||||
nonce: ByteArray? = null,
|
||||
expiresTime: Long,
|
||||
payload: ObjectPayload,
|
||||
stream: Long
|
||||
) : this(
|
||||
nonce,
|
||||
expiresTime,
|
||||
payload,
|
||||
payload.type?.number ?: throw IllegalArgumentException("payload must have type defined"),
|
||||
payload.version,
|
||||
stream
|
||||
)
|
||||
|
||||
val inventoryVector: InventoryVector
|
||||
get() {
|
||||
return InventoryVector(Bytes.truncate(cryptography().doubleSha512(
|
||||
nonce ?: throw IllegalStateException("nonce must be set"),
|
||||
payloadBytesWithoutNonce
|
||||
), 32))
|
||||
}
|
||||
|
||||
private val isEncrypted: Boolean
|
||||
get() = payload is Encrypted && !payload.isDecrypted
|
||||
|
||||
val isSigned: Boolean
|
||||
get() = payload.isSigned
|
||||
|
||||
private val bytesToSign: ByteArray
|
||||
get() {
|
||||
try {
|
||||
val out = ByteArrayOutputStream()
|
||||
writeHeaderWithoutNonce(out)
|
||||
payload.writeBytesToSign(out)
|
||||
return out.toByteArray()
|
||||
} catch (e: IOException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun sign(key: PrivateKey) {
|
||||
if (payload.isSigned) {
|
||||
payload.signature = cryptography().getSignature(bytesToSign, key)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
fun decrypt(key: PrivateKey) {
|
||||
if (payload is Encrypted) {
|
||||
payload.decrypt(key.privateEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
fun decrypt(privateEncryptionKey: ByteArray) {
|
||||
if (payload is Encrypted) {
|
||||
payload.decrypt(privateEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(publicEncryptionKey: ByteArray) {
|
||||
if (payload is Encrypted) {
|
||||
payload.encrypt(publicEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(publicKey: Pubkey) {
|
||||
try {
|
||||
if (payload is Encrypted) {
|
||||
payload.encrypt(publicKey.encryptionKey)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun isSignatureValid(pubkey: Pubkey): Boolean {
|
||||
if (isEncrypted) throw IllegalStateException("Payload must be decrypted first")
|
||||
return cryptography().isSignatureValid(bytesToSign, payload.signature ?: return false, pubkey)
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
out.write(nonce ?: ByteArray(8))
|
||||
out.write(payloadBytesWithoutNonce)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
buffer.put(nonce ?: ByteArray(8))
|
||||
buffer.put(payloadBytesWithoutNonce)
|
||||
}
|
||||
|
||||
private fun writeHeaderWithoutNonce(out: OutputStream) {
|
||||
Encode.int64(expiresTime, out)
|
||||
Encode.int32(type, out)
|
||||
Encode.varInt(version, out)
|
||||
Encode.varInt(stream, out)
|
||||
}
|
||||
|
||||
val payloadBytesWithoutNonce: ByteArray by lazy {
|
||||
val out = ByteArrayOutputStream()
|
||||
writeHeaderWithoutNonce(out)
|
||||
payload.write(out)
|
||||
out.toByteArray()
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var nonce: ByteArray? = null
|
||||
private var expiresTime: Long = 0
|
||||
private var objectType: Long? = null
|
||||
private var streamNumber: Long = 0
|
||||
private var payload: ObjectPayload? = null
|
||||
|
||||
fun nonce(nonce: ByteArray): Builder {
|
||||
this.nonce = nonce
|
||||
return this
|
||||
}
|
||||
|
||||
fun expiresTime(expiresTime: Long): Builder {
|
||||
this.expiresTime = expiresTime
|
||||
return this
|
||||
}
|
||||
|
||||
fun objectType(objectType: Long): Builder {
|
||||
this.objectType = objectType
|
||||
return this
|
||||
}
|
||||
|
||||
fun objectType(objectType: ObjectType): Builder {
|
||||
this.objectType = objectType.number
|
||||
return this
|
||||
}
|
||||
|
||||
fun stream(streamNumber: Long): Builder {
|
||||
this.streamNumber = streamNumber
|
||||
return this
|
||||
}
|
||||
|
||||
fun payload(payload: ObjectPayload): Builder {
|
||||
this.payload = payload
|
||||
if (this.objectType == null)
|
||||
this.objectType = payload.type?.number
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ObjectMessage {
|
||||
return ObjectMessage(
|
||||
nonce = nonce,
|
||||
expiresTime = expiresTime,
|
||||
type = objectType!!,
|
||||
version = payload!!.version,
|
||||
stream = if (streamNumber > 0) streamNumber else payload!!.stream,
|
||||
payload = payload!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ObjectMessage) return false
|
||||
return expiresTime == other.expiresTime &&
|
||||
type == other.type &&
|
||||
version == other.version &&
|
||||
stream == other.stream &&
|
||||
payload == other.payload
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = Arrays.hashCode(nonce)
|
||||
result = 31 * result + (expiresTime xor expiresTime.ushr(32)).toInt()
|
||||
result = 31 * result + (type xor type.ushr(32)).toInt()
|
||||
result = 31 * result + (version xor version.ushr(32)).toInt()
|
||||
result = 31 * result + (stream xor stream.ushr(32)).toInt()
|
||||
result = 31 * result + (payload.hashCode())
|
||||
return result
|
||||
}
|
||||
}
|
753
core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt
Normal file
753
core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt
Normal file
@ -0,0 +1,753 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.*
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
||||
import ch.dissem.bitmessage.entity.payload.Msg
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Attachment
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.factory.ExtendedEncodingFactory
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.utils.*
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
internal 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)
|
||||
}
|
||||
|
||||
internal fun ackData(type: Plaintext.Type, ackData: ByteArray?): ByteArray? {
|
||||
if (ackData != null) {
|
||||
return ackData
|
||||
} else if (type == MSG) {
|
||||
return cryptography().randomBytes(Msg.ACK_LENGTH)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The unencrypted message to be sent by 'msg' or 'broadcast'.
|
||||
*/
|
||||
class Plaintext private constructor(
|
||||
val type: Type,
|
||||
val from: BitmessageAddress,
|
||||
to: BitmessageAddress?,
|
||||
val encodingCode: Long,
|
||||
val message: ByteArray,
|
||||
val ackData: ByteArray?,
|
||||
ackMessage: Lazy<ObjectMessage?> = lazy { Factory.createAck(from, ackData, ttl) },
|
||||
val conversationId: UUID = UUID.randomUUID(),
|
||||
var inventoryVector: InventoryVector? = null,
|
||||
var signature: ByteArray? = null,
|
||||
sent: Long? = null,
|
||||
val received: Long? = null,
|
||||
var initialHash: ByteArray? = null,
|
||||
ttl: Long = TTL.msg,
|
||||
val labels: MutableSet<Label> = HashSet(),
|
||||
status: Status
|
||||
) : Streamable {
|
||||
|
||||
var id: Any? = null
|
||||
set(id) {
|
||||
if (this.id != null) throw IllegalStateException("ID already set")
|
||||
field = id
|
||||
}
|
||||
|
||||
var to: BitmessageAddress? = to
|
||||
set(to) {
|
||||
if (to == null) {
|
||||
return
|
||||
}
|
||||
if (this.to != null) {
|
||||
if (this.to!!.version != 0L)
|
||||
throw IllegalStateException("Correct address already set")
|
||||
if (!Arrays.equals(this.to!!.ripe, to.ripe)) {
|
||||
throw IllegalArgumentException("RIPEs don't match")
|
||||
}
|
||||
}
|
||||
field = to
|
||||
}
|
||||
|
||||
val stream: Long
|
||||
get() = to?.stream ?: from.stream
|
||||
|
||||
val extendedData: ExtendedEncoding? by lazy {
|
||||
if (encodingCode == EXTENDED.code) {
|
||||
ExtendedEncodingFactory.unzip(message)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val ackMessage: ObjectMessage? by ackMessage
|
||||
|
||||
var status: Status = status
|
||||
set(status) {
|
||||
if (status != Status.RECEIVED && sent == null && status != Status.DRAFT) {
|
||||
sent = UnixTime.now
|
||||
}
|
||||
field = status
|
||||
}
|
||||
|
||||
val encoding: Encoding? by lazy { Encoding.fromCode(encodingCode) }
|
||||
var sent: Long? = sent
|
||||
private set
|
||||
var retries: Int = 0
|
||||
private set
|
||||
var nextTry: Long? = null
|
||||
private set
|
||||
val ttl: Long = ttl
|
||||
@JvmName("getTTL") get
|
||||
|
||||
constructor(
|
||||
type: Type,
|
||||
from: BitmessageAddress,
|
||||
to: BitmessageAddress?,
|
||||
encoding: Encoding,
|
||||
message: ByteArray,
|
||||
ackData: ByteArray? = null,
|
||||
conversationId: UUID = UUID.randomUUID(),
|
||||
inventoryVector: InventoryVector? = null,
|
||||
signature: ByteArray? = null,
|
||||
received: Long? = null,
|
||||
initialHash: ByteArray? = null,
|
||||
ttl: Long = TTL.msg,
|
||||
labels: MutableSet<Label> = HashSet(),
|
||||
status: Status
|
||||
) : this(
|
||||
type = type,
|
||||
from = from,
|
||||
to = to,
|
||||
encodingCode = encoding.code,
|
||||
message = message,
|
||||
ackData = ackData(type, ackData),
|
||||
conversationId = conversationId,
|
||||
inventoryVector = inventoryVector,
|
||||
signature = signature,
|
||||
received = received,
|
||||
initialHash = initialHash,
|
||||
ttl = ttl,
|
||||
labels = labels,
|
||||
status = status
|
||||
)
|
||||
|
||||
constructor(
|
||||
type: Type,
|
||||
from: BitmessageAddress,
|
||||
to: BitmessageAddress?,
|
||||
encoding: Long,
|
||||
message: ByteArray,
|
||||
ackMessage: ByteArray?,
|
||||
conversationId: UUID = UUID.randomUUID(),
|
||||
inventoryVector: InventoryVector? = null,
|
||||
signature: ByteArray? = null,
|
||||
received: Long? = null,
|
||||
initialHash: ByteArray? = null,
|
||||
ttl: Long = TTL.msg,
|
||||
labels: MutableSet<Label> = HashSet(),
|
||||
status: Status
|
||||
) : this(
|
||||
type = type,
|
||||
from = from,
|
||||
to = to,
|
||||
encodingCode = encoding,
|
||||
message = message,
|
||||
ackData = null,
|
||||
ackMessage = lazy {
|
||||
if (ackMessage != null && ackMessage.isNotEmpty()) {
|
||||
Factory.getObjectMessage(
|
||||
3,
|
||||
ByteArrayInputStream(ackMessage),
|
||||
ackMessage.size)
|
||||
} else null
|
||||
},
|
||||
conversationId = conversationId,
|
||||
inventoryVector = inventoryVector,
|
||||
signature = signature,
|
||||
received = received,
|
||||
initialHash = initialHash,
|
||||
ttl = ttl,
|
||||
labels = labels,
|
||||
status = status
|
||||
)
|
||||
|
||||
constructor(
|
||||
type: Type,
|
||||
from: BitmessageAddress,
|
||||
to: BitmessageAddress? = null,
|
||||
encoding: Encoding = SIMPLE,
|
||||
subject: String,
|
||||
body: String,
|
||||
ackData: ByteArray? = null,
|
||||
conversationId: UUID = UUID.randomUUID(),
|
||||
ttl: Long = TTL.msg,
|
||||
labels: MutableSet<Label> = HashSet(),
|
||||
status: Status = Status.DRAFT
|
||||
) : this(
|
||||
type = type,
|
||||
from = from,
|
||||
to = to,
|
||||
encoding = encoding,
|
||||
message = message(encoding, subject, body),
|
||||
ackData = ackData(type, ackData),
|
||||
conversationId = conversationId,
|
||||
inventoryVector = null,
|
||||
signature = null,
|
||||
received = null,
|
||||
initialHash = null,
|
||||
ttl = ttl,
|
||||
labels = labels,
|
||||
status = status
|
||||
)
|
||||
|
||||
constructor(builder: Builder) : this(
|
||||
type = builder.type,
|
||||
from = builder.from ?: throw IllegalStateException("sender identity not set"),
|
||||
to = builder.to,
|
||||
encodingCode = builder.encoding,
|
||||
message = builder.message,
|
||||
ackData = builder.ackData,
|
||||
ackMessage = lazy {
|
||||
val ackMsg = builder.ackMessage
|
||||
if (ackMsg != null && ackMsg.isNotEmpty()) {
|
||||
Factory.getObjectMessage(
|
||||
3,
|
||||
ByteArrayInputStream(ackMsg),
|
||||
ackMsg.size)
|
||||
} else {
|
||||
Factory.createAck(builder.from!!, builder.ackData, builder.ttl)
|
||||
}
|
||||
},
|
||||
conversationId = builder.conversation ?: UUID.randomUUID(),
|
||||
inventoryVector = builder.inventoryVector,
|
||||
signature = builder.signature,
|
||||
sent = builder.sent,
|
||||
received = builder.received,
|
||||
initialHash = null,
|
||||
ttl = builder.ttl,
|
||||
labels = builder.labels,
|
||||
status = builder.status ?: Status.RECEIVED
|
||||
) {
|
||||
id = builder.id
|
||||
}
|
||||
|
||||
fun write(out: OutputStream, includeSignature: Boolean) {
|
||||
Encode.varInt(from.version, out)
|
||||
Encode.varInt(from.stream, out)
|
||||
if (from.pubkey == null) {
|
||||
Encode.int32(0, out)
|
||||
val empty = ByteArray(64)
|
||||
out.write(empty)
|
||||
out.write(empty)
|
||||
if (from.version >= 3) {
|
||||
Encode.varInt(0, out)
|
||||
Encode.varInt(0, out)
|
||||
}
|
||||
} else {
|
||||
Encode.int32(from.pubkey!!.behaviorBitfield, out)
|
||||
out.write(from.pubkey!!.signingKey, 1, 64)
|
||||
out.write(from.pubkey!!.encryptionKey, 1, 64)
|
||||
if (from.version >= 3) {
|
||||
Encode.varInt(from.pubkey!!.nonceTrialsPerByte, out)
|
||||
Encode.varInt(from.pubkey!!.extraBytes, out)
|
||||
}
|
||||
}
|
||||
if (type == MSG) {
|
||||
out.write(to!!.ripe)
|
||||
}
|
||||
Encode.varInt(encodingCode, out)
|
||||
Encode.varInt(message.size, out)
|
||||
out.write(message)
|
||||
if (type == MSG) {
|
||||
if (to?.has(Feature.DOES_ACK) ?: false) {
|
||||
val ack = ByteArrayOutputStream()
|
||||
ackMessage?.write(ack)
|
||||
Encode.varBytes(ack.toByteArray(), out)
|
||||
} else {
|
||||
Encode.varInt(0, out)
|
||||
}
|
||||
}
|
||||
if (includeSignature) {
|
||||
if (signature == null) {
|
||||
Encode.varInt(0, out)
|
||||
} else {
|
||||
Encode.varBytes(signature!!, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun write(buffer: ByteBuffer, includeSignature: Boolean) {
|
||||
Encode.varInt(from.version, buffer)
|
||||
Encode.varInt(from.stream, buffer)
|
||||
if (from.pubkey == null) {
|
||||
Encode.int32(0, buffer)
|
||||
val empty = ByteArray(64)
|
||||
buffer.put(empty)
|
||||
buffer.put(empty)
|
||||
if (from.version >= 3) {
|
||||
Encode.varInt(0, buffer)
|
||||
Encode.varInt(0, buffer)
|
||||
}
|
||||
} else {
|
||||
Encode.int32(from.pubkey!!.behaviorBitfield, buffer)
|
||||
buffer.put(from.pubkey!!.signingKey, 1, 64)
|
||||
buffer.put(from.pubkey!!.encryptionKey, 1, 64)
|
||||
if (from.version >= 3) {
|
||||
Encode.varInt(from.pubkey!!.nonceTrialsPerByte, buffer)
|
||||
Encode.varInt(from.pubkey!!.extraBytes, buffer)
|
||||
}
|
||||
}
|
||||
if (type == MSG) {
|
||||
buffer.put(to!!.ripe)
|
||||
}
|
||||
Encode.varInt(encodingCode, buffer)
|
||||
Encode.varBytes(message, buffer)
|
||||
if (type == MSG) {
|
||||
if (to!!.has(Feature.DOES_ACK) && ackMessage != null) {
|
||||
Encode.varBytes(Encode.bytes(ackMessage!!), buffer)
|
||||
} else {
|
||||
Encode.varInt(0, buffer)
|
||||
}
|
||||
}
|
||||
if (includeSignature) {
|
||||
val sig = signature
|
||||
if (sig == null) {
|
||||
Encode.varInt(0, buffer)
|
||||
} else {
|
||||
Encode.varBytes(sig, buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
write(out, true)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
write(buffer, true)
|
||||
}
|
||||
|
||||
fun updateNextTry() {
|
||||
if (to != null) {
|
||||
if (nextTry == null) {
|
||||
if (sent != null && to!!.has(Feature.DOES_ACK)) {
|
||||
nextTry = UnixTime.now + ttl
|
||||
retries++
|
||||
}
|
||||
} else {
|
||||
nextTry = nextTry!! + (1 shl retries) * ttl
|
||||
retries++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val subject: String?
|
||||
get() {
|
||||
val s = Scanner(ByteArrayInputStream(message), "UTF-8")
|
||||
val firstLine = s.nextLine()
|
||||
if (encodingCode == EXTENDED.code) {
|
||||
if (Message.TYPE == extendedData?.type) {
|
||||
return (extendedData!!.content as? Message)?.subject
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else if (encodingCode == SIMPLE.code) {
|
||||
return firstLine.substring("Subject:".length).trim { it <= ' ' }
|
||||
} else if (firstLine.length > 50) {
|
||||
return firstLine.substring(0, 50).trim { it <= ' ' } + "..."
|
||||
} else {
|
||||
return firstLine
|
||||
}
|
||||
}
|
||||
|
||||
val text: String?
|
||||
get() {
|
||||
if (encodingCode == EXTENDED.code) {
|
||||
if (Message.TYPE == extendedData?.type) {
|
||||
return (extendedData?.content as Message?)?.body
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
val text = String(message)
|
||||
if (encodingCode == SIMPLE.code) {
|
||||
return text.substring(text.indexOf("\nBody:") + 6)
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : ExtendedEncoding.ExtendedType> getExtendedData(type: Class<T>): T? {
|
||||
val extendedData = extendedData ?: return null
|
||||
if (type.isInstance(extendedData.content)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return extendedData.content as T
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val parents: List<InventoryVector>
|
||||
get() {
|
||||
val extendedData = extendedData ?: return emptyList()
|
||||
if (Message.TYPE == extendedData.type) {
|
||||
return (extendedData.content as Message).parents
|
||||
} else {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
val files: List<Attachment>
|
||||
get() {
|
||||
val extendedData = extendedData ?: return emptyList()
|
||||
if (Message.TYPE == extendedData.type) {
|
||||
return (extendedData.content as Message).files
|
||||
} else {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Plaintext) return false
|
||||
return encoding == other.encoding &&
|
||||
from == other.from &&
|
||||
Arrays.equals(message, other.message) &&
|
||||
ackMessage == other.ackMessage &&
|
||||
Arrays.equals(to?.ripe, other.to?.ripe) &&
|
||||
Arrays.equals(signature, other.signature) &&
|
||||
status == other.status &&
|
||||
sent == other.sent &&
|
||||
received == other.received &&
|
||||
labels == other.labels
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels)
|
||||
}
|
||||
|
||||
fun addLabels(vararg labels: Label) {
|
||||
Collections.addAll(this.labels, *labels)
|
||||
}
|
||||
|
||||
fun addLabels(labels: Collection<Label>?) {
|
||||
if (labels != null) {
|
||||
this.labels.addAll(labels)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLabel(type: Label.Type) {
|
||||
labels.removeIf { it.type == type }
|
||||
}
|
||||
|
||||
fun isUnread(): Boolean {
|
||||
return labels.any { it.type == Label.Type.UNREAD }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val subject = subject
|
||||
if (subject?.isNotEmpty() ?: false) {
|
||||
return subject!!
|
||||
} else {
|
||||
return Strings.hex(
|
||||
initialHash ?: return super.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Encoding constructor(code: Long) {
|
||||
IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3);
|
||||
|
||||
var code: Long = 0
|
||||
internal set
|
||||
|
||||
init {
|
||||
this.code = code
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic fun fromCode(code: Long): Encoding? {
|
||||
for (e in values()) {
|
||||
if (e.code == code) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
DRAFT,
|
||||
// For sent messages
|
||||
PUBKEY_REQUESTED,
|
||||
DOING_PROOF_OF_WORK,
|
||||
SENT,
|
||||
SENT_ACKNOWLEDGED,
|
||||
RECEIVED
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
MSG, BROADCAST
|
||||
}
|
||||
|
||||
class Builder(internal val type: Type) {
|
||||
internal var id: Any? = null
|
||||
internal var inventoryVector: InventoryVector? = null
|
||||
internal var from: BitmessageAddress? = null
|
||||
internal var to: BitmessageAddress? = null
|
||||
private var addressVersion: Long = 0
|
||||
private var stream: Long = 0
|
||||
private var behaviorBitfield: Int = 0
|
||||
private var publicSigningKey: ByteArray? = null
|
||||
private var publicEncryptionKey: ByteArray? = null
|
||||
private var nonceTrialsPerByte: Long = 0
|
||||
private var extraBytes: Long = 0
|
||||
private var destinationRipe: ByteArray? = null
|
||||
internal var encoding: Long = 0
|
||||
internal var message = ByteArray(0)
|
||||
internal var ackData: ByteArray? = null
|
||||
internal var ackMessage: ByteArray? = null
|
||||
internal var signature: ByteArray? = null
|
||||
internal var sent: Long? = null
|
||||
internal var received: Long? = null
|
||||
internal var status: Status? = null
|
||||
internal val labels = LinkedHashSet<Label>()
|
||||
internal var ttl: Long = 0
|
||||
internal var retries: Int = 0
|
||||
internal var nextTry: Long? = null
|
||||
internal var conversation: UUID? = null
|
||||
|
||||
fun id(id: Any): Builder {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
fun IV(iv: InventoryVector?): Builder {
|
||||
this.inventoryVector = iv
|
||||
return this
|
||||
}
|
||||
|
||||
fun from(address: BitmessageAddress): Builder {
|
||||
from = address
|
||||
return this
|
||||
}
|
||||
|
||||
fun to(address: BitmessageAddress?): Builder {
|
||||
if (address != null) {
|
||||
if (type != MSG && to != null)
|
||||
throw IllegalArgumentException("recipient address only allowed for msg")
|
||||
to = address
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun addressVersion(addressVersion: Long): Builder {
|
||||
this.addressVersion = addressVersion
|
||||
return this
|
||||
}
|
||||
|
||||
fun stream(stream: Long): Builder {
|
||||
this.stream = stream
|
||||
return this
|
||||
}
|
||||
|
||||
fun behaviorBitfield(behaviorBitfield: Int): Builder {
|
||||
this.behaviorBitfield = behaviorBitfield
|
||||
return this
|
||||
}
|
||||
|
||||
fun publicSigningKey(publicSigningKey: ByteArray): Builder {
|
||||
this.publicSigningKey = publicSigningKey
|
||||
return this
|
||||
}
|
||||
|
||||
fun publicEncryptionKey(publicEncryptionKey: ByteArray): Builder {
|
||||
this.publicEncryptionKey = publicEncryptionKey
|
||||
return this
|
||||
}
|
||||
|
||||
fun nonceTrialsPerByte(nonceTrialsPerByte: Long): Builder {
|
||||
this.nonceTrialsPerByte = nonceTrialsPerByte
|
||||
return this
|
||||
}
|
||||
|
||||
fun extraBytes(extraBytes: Long): Builder {
|
||||
this.extraBytes = extraBytes
|
||||
return this
|
||||
}
|
||||
|
||||
fun destinationRipe(ripe: ByteArray?): Builder {
|
||||
if (type != MSG && ripe != null) throw IllegalArgumentException("ripe only allowed for msg")
|
||||
this.destinationRipe = ripe
|
||||
return this
|
||||
}
|
||||
|
||||
fun encoding(encoding: Encoding): Builder {
|
||||
this.encoding = encoding.code
|
||||
return this
|
||||
}
|
||||
|
||||
fun encoding(encoding: Long): Builder {
|
||||
this.encoding = encoding
|
||||
return this
|
||||
}
|
||||
|
||||
fun message(message: ExtendedEncoding): Builder {
|
||||
this.encoding = EXTENDED.code
|
||||
this.message = message.zip()
|
||||
return this
|
||||
}
|
||||
|
||||
fun message(subject: String, message: String): Builder {
|
||||
try {
|
||||
this.encoding = SIMPLE.code
|
||||
this.message = "Subject:$subject\nBody:$message".toByteArray(charset("UTF-8"))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun message(message: ByteArray): Builder {
|
||||
this.message = message
|
||||
return this
|
||||
}
|
||||
|
||||
fun ackMessage(ack: ByteArray?): Builder {
|
||||
if (type != MSG && ack != null) throw IllegalArgumentException("ackMessage only allowed for msg")
|
||||
this.ackMessage = ack
|
||||
return this
|
||||
}
|
||||
|
||||
fun ackData(ackData: ByteArray?): Builder {
|
||||
if (type != MSG && ackData != null)
|
||||
throw IllegalArgumentException("ackMessage only allowed for msg")
|
||||
this.ackData = ackData
|
||||
return this
|
||||
}
|
||||
|
||||
fun signature(signature: ByteArray): Builder {
|
||||
this.signature = signature
|
||||
return this
|
||||
}
|
||||
|
||||
fun sent(sent: Long?): Builder {
|
||||
this.sent = sent
|
||||
return this
|
||||
}
|
||||
|
||||
fun received(received: Long?): Builder {
|
||||
this.received = received
|
||||
return this
|
||||
}
|
||||
|
||||
fun status(status: Status): Builder {
|
||||
this.status = status
|
||||
return this
|
||||
}
|
||||
|
||||
fun labels(labels: Collection<Label>): Builder {
|
||||
this.labels.addAll(labels)
|
||||
return this
|
||||
}
|
||||
|
||||
fun ttl(ttl: Long): Builder {
|
||||
this.ttl = ttl
|
||||
return this
|
||||
}
|
||||
|
||||
fun retries(retries: Int): Builder {
|
||||
this.retries = retries
|
||||
return this
|
||||
}
|
||||
|
||||
fun nextTry(nextTry: Long?): Builder {
|
||||
this.nextTry = nextTry
|
||||
return this
|
||||
}
|
||||
|
||||
fun conversation(id: UUID): Builder {
|
||||
this.conversation = id
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Plaintext {
|
||||
if (from == null) {
|
||||
from = BitmessageAddress(Factory.createPubkey(
|
||||
addressVersion,
|
||||
stream,
|
||||
publicSigningKey!!,
|
||||
publicEncryptionKey!!,
|
||||
nonceTrialsPerByte,
|
||||
extraBytes,
|
||||
behaviorBitfield
|
||||
))
|
||||
}
|
||||
if (to == null && type != Type.BROADCAST && destinationRipe != null) {
|
||||
to = BitmessageAddress(0, 0, destinationRipe!!)
|
||||
}
|
||||
if (type == MSG && ackMessage == null && ackData == null) {
|
||||
ackData = cryptography().randomBytes(Msg.ACK_LENGTH)
|
||||
}
|
||||
if (ttl <= 0) {
|
||||
ttl = TTL.msg
|
||||
}
|
||||
return Plaintext(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic fun read(type: Type, `in`: InputStream): Plaintext {
|
||||
return readWithoutSignature(type, `in`)
|
||||
.signature(Decode.varBytes(`in`))
|
||||
.received(UnixTime.now)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic fun readWithoutSignature(type: Type, `in`: InputStream): Plaintext.Builder {
|
||||
val version = Decode.varInt(`in`)
|
||||
return Builder(type)
|
||||
.addressVersion(version)
|
||||
.stream(Decode.varInt(`in`))
|
||||
.behaviorBitfield(Decode.int32(`in`))
|
||||
.publicSigningKey(Decode.bytes(`in`, 64))
|
||||
.publicEncryptionKey(Decode.bytes(`in`, 64))
|
||||
.nonceTrialsPerByte(if (version >= 3) Decode.varInt(`in`) else 0)
|
||||
.extraBytes(if (version >= 3) Decode.varInt(`in`) else 0)
|
||||
.destinationRipe(if (type == MSG) Decode.bytes(`in`, 20) else null)
|
||||
.encoding(Decode.varInt(`in`))
|
||||
.message(Decode.varBytes(`in`))
|
||||
.ackMessage(if (type == MSG) Decode.varBytes(`in`) else null)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
interface PlaintextHolder {
|
||||
val plaintext: Plaintext?
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.io.Serializable
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* An object that can be written to an [OutputStream]
|
||||
*/
|
||||
interface Streamable : Serializable {
|
||||
fun write(out: OutputStream)
|
||||
|
||||
fun write(buffer: ByteBuffer)
|
||||
}
|
40
core/src/main/kotlin/ch/dissem/bitmessage/entity/VerAck.kt
Normal file
40
core/src/main/kotlin/ch/dissem/bitmessage/entity/VerAck.kt
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* The 'verack' command answers a 'version' command, accepting the other node's version.
|
||||
*/
|
||||
class VerAck : MessagePayload {
|
||||
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.VERACK
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
// 'verack' doesn't have any payload, so there is nothing to write
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
// 'verack' doesn't have any payload, so there is nothing to write
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val serialVersionUID = -4302074845199181687L
|
||||
}
|
||||
}
|
204
core/src/main/kotlin/ch/dissem/bitmessage/entity/Version.kt
Normal file
204
core/src/main/kotlin/ch/dissem/bitmessage/entity/Version.kt
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity
|
||||
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* The 'version' command advertises this node's latest supported protocol version upon initiation.
|
||||
*/
|
||||
class Version constructor(
|
||||
/**
|
||||
* Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's
|
||||
* version is lower but continue with the connection if it is higher.
|
||||
*/
|
||||
val version: Int = BitmessageContext.CURRENT_VERSION,
|
||||
|
||||
/**
|
||||
* bitfield of features to be enabled for this connection
|
||||
*/
|
||||
val services: Long = Version.Service.getServiceFlag(Version.Service.NODE_NETWORK),
|
||||
|
||||
/**
|
||||
* standard UNIX timestamp in seconds
|
||||
*/
|
||||
val timestamp: Long = UnixTime.now,
|
||||
|
||||
/**
|
||||
* The network address of the node receiving this message (not including the time or stream number)
|
||||
*/
|
||||
val addrRecv: NetworkAddress,
|
||||
|
||||
/**
|
||||
* The network address of the node emitting this message (not including the time or stream number and the ip itself
|
||||
* is ignored by the receiver)
|
||||
*/
|
||||
val addrFrom: NetworkAddress,
|
||||
|
||||
/**
|
||||
* Random nonce used to detect connections to self.
|
||||
*/
|
||||
val nonce: Long,
|
||||
|
||||
/**
|
||||
* User Agent (0x00 if string is 0 bytes long). Sending nodes must not include a user_agent longer than 5000 bytes.
|
||||
*/
|
||||
val userAgent: String = "/Jabit:0.0.1/",
|
||||
|
||||
/**
|
||||
* The stream numbers that the emitting node is interested in. Sending nodes must not include more than 160000
|
||||
* stream numbers.
|
||||
*/
|
||||
val streams: LongArray = longArrayOf(1)
|
||||
) : MessagePayload {
|
||||
|
||||
fun provides(service: Service?): Boolean {
|
||||
return service != null && service.isEnabled(services)
|
||||
}
|
||||
|
||||
override val command: MessagePayload.Command = MessagePayload.Command.VERSION
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
Encode.int32(version, out)
|
||||
Encode.int64(services, out)
|
||||
Encode.int64(timestamp, out)
|
||||
addrRecv.write(out, true)
|
||||
addrFrom.write(out, true)
|
||||
Encode.int64(nonce, out)
|
||||
Encode.varString(userAgent, out)
|
||||
Encode.varIntList(streams, out)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
Encode.int32(version, buffer)
|
||||
Encode.int64(services, buffer)
|
||||
Encode.int64(timestamp, buffer)
|
||||
addrRecv.write(buffer, true)
|
||||
addrFrom.write(buffer, true)
|
||||
Encode.int64(nonce, buffer)
|
||||
Encode.varString(userAgent, buffer)
|
||||
Encode.varIntList(streams, buffer)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var version: Int = 0
|
||||
private var services: Long = 0
|
||||
private var timestamp: Long = 0
|
||||
private var addrRecv: NetworkAddress? = null
|
||||
private var addrFrom: NetworkAddress? = null
|
||||
private var nonce: Long = 0
|
||||
private var userAgent: String? = null
|
||||
private var streamNumbers: LongArray? = null
|
||||
|
||||
fun defaults(clientNonce: Long): Builder {
|
||||
version = BitmessageContext.CURRENT_VERSION
|
||||
services = Service.getServiceFlag(Service.NODE_NETWORK)
|
||||
timestamp = UnixTime.now
|
||||
userAgent = "/Jabit:0.0.1/"
|
||||
streamNumbers = longArrayOf(1)
|
||||
nonce = clientNonce
|
||||
return this
|
||||
}
|
||||
|
||||
fun version(version: Int): Builder {
|
||||
this.version = version
|
||||
return this
|
||||
}
|
||||
|
||||
fun services(vararg services: Service): Builder {
|
||||
this.services = Service.getServiceFlag(*services)
|
||||
return this
|
||||
}
|
||||
|
||||
fun services(services: Long): Builder {
|
||||
this.services = services
|
||||
return this
|
||||
}
|
||||
|
||||
fun timestamp(timestamp: Long): Builder {
|
||||
this.timestamp = timestamp
|
||||
return this
|
||||
}
|
||||
|
||||
fun addrRecv(addrRecv: NetworkAddress): Builder {
|
||||
this.addrRecv = addrRecv
|
||||
return this
|
||||
}
|
||||
|
||||
fun addrFrom(addrFrom: NetworkAddress): Builder {
|
||||
this.addrFrom = addrFrom
|
||||
return this
|
||||
}
|
||||
|
||||
fun nonce(nonce: Long): Builder {
|
||||
this.nonce = nonce
|
||||
return this
|
||||
}
|
||||
|
||||
fun userAgent(userAgent: String): Builder {
|
||||
this.userAgent = userAgent
|
||||
return this
|
||||
}
|
||||
|
||||
fun streams(vararg streamNumbers: Long): Builder {
|
||||
this.streamNumbers = streamNumbers
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Version {
|
||||
val addrRecv = this.addrRecv
|
||||
val addrFrom = this.addrFrom
|
||||
if (addrRecv == null || addrFrom == null) {
|
||||
throw IllegalStateException("Receiving and sending address must be set")
|
||||
}
|
||||
|
||||
return Version(
|
||||
version = version,
|
||||
services = services,
|
||||
timestamp = timestamp,
|
||||
addrRecv = addrRecv, addrFrom = addrFrom,
|
||||
nonce = nonce,
|
||||
userAgent = userAgent ?: "/Jabit:0.0.1/",
|
||||
streams = streamNumbers ?: longArrayOf(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Service constructor(internal var flag: Long) {
|
||||
// TODO: NODE_SSL(2);
|
||||
NODE_NETWORK(1);
|
||||
|
||||
fun isEnabled(flag: Long): Boolean {
|
||||
return (flag and this.flag) != 0L
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getServiceFlag(vararg services: Service): Long {
|
||||
var flag: Long = 0
|
||||
for (service in services) {
|
||||
flag = flag or service.flag
|
||||
}
|
||||
return flag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Encrypted
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
||||
import ch.dissem.bitmessage.entity.PlaintextHolder
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Users who are subscribed to the sending address will see the message appear in their inbox.
|
||||
* Broadcasts are version 4 or 5.
|
||||
*/
|
||||
abstract class Broadcast protected constructor(version: Long, override val stream: Long, protected var encrypted: CryptoBox?, override var plaintext: Plaintext?) : ObjectPayload(version), Encrypted, PlaintextHolder {
|
||||
|
||||
override val isSigned: Boolean = true
|
||||
|
||||
override var signature: ByteArray?
|
||||
get() = plaintext?.signature
|
||||
set(signature) {
|
||||
plaintext?.signature = signature ?: throw IllegalStateException("no plaintext data available")
|
||||
}
|
||||
|
||||
override fun encrypt(publicKey: ByteArray) {
|
||||
this.encrypted = CryptoBox(plaintext ?: throw IllegalStateException("no plaintext data available"), publicKey)
|
||||
}
|
||||
|
||||
fun encrypt() {
|
||||
encrypt(cryptography().createPublicKey(plaintext?.from?.publicDecryptionKey ?: return))
|
||||
}
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
override fun decrypt(privateKey: ByteArray) {
|
||||
plaintext = Plaintext.read(BROADCAST, encrypted?.decrypt(privateKey) ?: return)
|
||||
}
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
fun decrypt(address: BitmessageAddress) {
|
||||
decrypt(address.publicDecryptionKey)
|
||||
}
|
||||
|
||||
override val isDecrypted: Boolean
|
||||
get() = plaintext != null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Broadcast) return false
|
||||
return stream == other.stream && (encrypted == other.encrypted || plaintext == other.plaintext)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hash(stream)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getVersion(address: BitmessageAddress): Long {
|
||||
return if (address.version < 4) 4L else 5L
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.Streamable
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey.Companion.PRIVATE_KEY_SIZE
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.utils.*
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
|
||||
class CryptoBox : Streamable {
|
||||
|
||||
private val initializationVector: ByteArray
|
||||
private val curveType: Int
|
||||
private val R: ByteArray
|
||||
private val mac: ByteArray
|
||||
private var encrypted: ByteArray
|
||||
|
||||
constructor(data: Streamable, K: ByteArray) : this(Encode.bytes(data), K)
|
||||
|
||||
constructor(data: ByteArray, K: ByteArray) {
|
||||
curveType = 0x02CA
|
||||
|
||||
// 1. The destination public key is called K.
|
||||
// 2. Generate 16 random bytes using a secure random number generator. Call them IV.
|
||||
initializationVector = cryptography().randomBytes(16)
|
||||
|
||||
// 3. Generate a new random EC key pair with private key called r and public key called R.
|
||||
val r = cryptography().randomBytes(PRIVATE_KEY_SIZE)
|
||||
R = cryptography().createPublicKey(r)
|
||||
// 4. Do an EC point multiply with public key K and private key r. This gives you public key P.
|
||||
val P = cryptography().multiply(K, r)
|
||||
val X = Points.getX(P)
|
||||
// 5. Use the X component of public key P and calculate the SHA512 hash H.
|
||||
val H = cryptography().sha512(X)
|
||||
// 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
|
||||
val key_e = Arrays.copyOfRange(H, 0, 32)
|
||||
val key_m = Arrays.copyOfRange(H, 32, 64)
|
||||
// 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7.
|
||||
// 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text.
|
||||
encrypted = cryptography().crypt(true, data, key_e, initializationVector)
|
||||
// 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC.
|
||||
mac = calculateMac(key_m)
|
||||
|
||||
// The resulting data is: IV + R + cipher text + MAC
|
||||
}
|
||||
|
||||
private constructor(builder: Builder) {
|
||||
initializationVector = builder.initializationVector!!
|
||||
curveType = builder.curveType
|
||||
R = cryptography().createPoint(builder.xComponent!!, builder.yComponent!!)
|
||||
encrypted = builder.encrypted!!
|
||||
mac = builder.mac!!
|
||||
}
|
||||
|
||||
/**
|
||||
* @param k a private key, typically should be 32 bytes long
|
||||
* *
|
||||
* @return an InputStream yielding the decrypted data
|
||||
* *
|
||||
* @throws DecryptionFailedException if the payload can't be decrypted using this private key
|
||||
* *
|
||||
* @see [https://bitmessage.org/wiki/Encryption.Decryption](https://bitmessage.org/wiki/Encryption.Decryption)
|
||||
*/
|
||||
@Throws(DecryptionFailedException::class)
|
||||
fun decrypt(k: ByteArray): InputStream {
|
||||
// 1. The private key used to decrypt is called k.
|
||||
// 2. Do an EC point multiply with private key k and public key R. This gives you public key P.
|
||||
val P = cryptography().multiply(R, k)
|
||||
// 3. Use the X component of public key P and calculate the SHA512 hash H.
|
||||
val H = cryptography().sha512(Arrays.copyOfRange(P, 1, 33))
|
||||
// 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
|
||||
val key_e = Arrays.copyOfRange(H, 0, 32)
|
||||
val key_m = Arrays.copyOfRange(H, 32, 64)
|
||||
|
||||
// 5. Calculate MAC' with HMACSHA256, using key_m as salt and IV + R + cipher text as data.
|
||||
// 6. Compare MAC with MAC'. If not equal, decryption will fail.
|
||||
if (!Arrays.equals(mac, calculateMac(key_m))) {
|
||||
throw DecryptionFailedException()
|
||||
}
|
||||
|
||||
// 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key
|
||||
// and the cipher text as payload. The output is the padded input text.
|
||||
return ByteArrayInputStream(cryptography().crypt(false, encrypted, key_e, initializationVector))
|
||||
}
|
||||
|
||||
private fun calculateMac(key_m: ByteArray): ByteArray {
|
||||
val macData = ByteArrayOutputStream()
|
||||
writeWithoutMAC(macData)
|
||||
return cryptography().mac(key_m, macData.toByteArray())
|
||||
}
|
||||
|
||||
private fun writeWithoutMAC(out: OutputStream) {
|
||||
out.write(initializationVector)
|
||||
Encode.int16(curveType, out)
|
||||
writeCoordinateComponent(out, Points.getX(R))
|
||||
writeCoordinateComponent(out, Points.getY(R))
|
||||
out.write(encrypted)
|
||||
}
|
||||
|
||||
private fun writeCoordinateComponent(out: OutputStream, x: ByteArray) {
|
||||
val offset = Bytes.numberOfLeadingZeros(x)
|
||||
val length = x.size - offset
|
||||
Encode.int16(length, out)
|
||||
out.write(x, offset, length)
|
||||
}
|
||||
|
||||
private fun writeCoordinateComponent(buffer: ByteBuffer, x: ByteArray) {
|
||||
val offset = Bytes.numberOfLeadingZeros(x)
|
||||
val length = x.size - offset
|
||||
Encode.int16(length, buffer)
|
||||
buffer.put(x, offset, length)
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
writeWithoutMAC(out)
|
||||
out.write(mac)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
buffer.put(initializationVector)
|
||||
Encode.int16(curveType, buffer)
|
||||
writeCoordinateComponent(buffer, Points.getX(R))
|
||||
writeCoordinateComponent(buffer, Points.getY(R))
|
||||
buffer.put(encrypted)
|
||||
buffer.put(mac)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
internal var initializationVector: ByteArray? = null
|
||||
internal var curveType: Int = 0
|
||||
internal var xComponent: ByteArray? = null
|
||||
internal var yComponent: ByteArray? = null
|
||||
internal var encrypted: ByteArray? = null
|
||||
internal var mac: ByteArray? = null
|
||||
|
||||
fun IV(initializationVector: ByteArray): Builder {
|
||||
this.initializationVector = initializationVector
|
||||
return this
|
||||
}
|
||||
|
||||
fun curveType(curveType: Int): Builder {
|
||||
if (curveType != 0x2CA) LOG.trace("Unexpected curve type " + curveType)
|
||||
this.curveType = curveType
|
||||
return this
|
||||
}
|
||||
|
||||
fun X(xComponent: ByteArray): Builder {
|
||||
this.xComponent = xComponent
|
||||
return this
|
||||
}
|
||||
|
||||
fun Y(yComponent: ByteArray): Builder {
|
||||
this.yComponent = yComponent
|
||||
return this
|
||||
}
|
||||
|
||||
fun encrypted(encrypted: ByteArray): Builder {
|
||||
this.encrypted = encrypted
|
||||
return this
|
||||
}
|
||||
|
||||
fun MAC(mac: ByteArray): Builder {
|
||||
this.mac = mac
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): CryptoBox {
|
||||
return CryptoBox(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(CryptoBox::class.java)
|
||||
|
||||
@JvmStatic fun read(stream: InputStream, length: Int): CryptoBox {
|
||||
val counter = AccessCounter()
|
||||
return Builder()
|
||||
.IV(Decode.bytes(stream, 16, counter))
|
||||
.curveType(Decode.uint16(stream, counter))
|
||||
.X(Decode.shortVarBytes(stream, counter))
|
||||
.Y(Decode.shortVarBytes(stream, counter))
|
||||
.encrypted(Decode.bytes(stream, length - counter.length() - 32))
|
||||
.MAC(Decode.bytes(stream, 32))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* In cases we don't know what to do with an object, we just store its bytes and send it again - we don't really
|
||||
* have to know what it is.
|
||||
*/
|
||||
class GenericPayload(version: Long, override val stream: Long, val data: ByteArray) : ObjectPayload(version) {
|
||||
|
||||
override val type: ObjectType? = null
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
out.write(data)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
buffer.put(data)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is GenericPayload) return false
|
||||
|
||||
if (stream != other.stream) return false
|
||||
return Arrays.equals(data, other.data)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = (stream xor stream.ushr(32)).toInt()
|
||||
result = 31 * result + Arrays.hashCode(data)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(version: Long, stream: Long, `is`: InputStream, length: Int): GenericPayload {
|
||||
return GenericPayload(version, stream, Decode.bytes(`is`, length))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Request for a public key.
|
||||
*/
|
||||
class GetPubkey : ObjectPayload {
|
||||
|
||||
override val type: ObjectType = ObjectType.GET_PUBKEY
|
||||
override val stream: Long
|
||||
|
||||
/**
|
||||
* @return an array of bytes that represent either the ripe, or the tag of an address, depending on the
|
||||
* * address version.
|
||||
*/
|
||||
val ripeTag: ByteArray
|
||||
|
||||
constructor(address: BitmessageAddress) : super(address.version) {
|
||||
this.stream = address.stream
|
||||
this.ripeTag = if (address.version < 4) address.ripe else
|
||||
address.tag ?: throw IllegalStateException("Address of version 4 without tag shouldn't exist!")
|
||||
}
|
||||
|
||||
private constructor(version: Long, stream: Long, ripeOrTag: ByteArray) : super(version) {
|
||||
this.stream = stream
|
||||
this.ripeTag = ripeOrTag
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
out.write(ripeTag)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
buffer.put(ripeTag)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(`is`: InputStream, stream: Long, length: Int, version: Long): GetPubkey {
|
||||
return GetPubkey(version, stream, Decode.bytes(`is`, length))
|
||||
}
|
||||
}
|
||||
}
|
103
core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Msg.kt
Normal file
103
core/src/main/kotlin/ch/dissem/bitmessage/entity/payload/Msg.kt
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.Encrypted
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
||||
import ch.dissem.bitmessage.entity.PlaintextHolder
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Used for person-to-person messages.
|
||||
*/
|
||||
class Msg : ObjectPayload, Encrypted, PlaintextHolder {
|
||||
|
||||
override val stream: Long
|
||||
private var encrypted: CryptoBox?
|
||||
override var plaintext: Plaintext?
|
||||
private set
|
||||
|
||||
private constructor(stream: Long, encrypted: CryptoBox) : super(1) {
|
||||
this.stream = stream
|
||||
this.encrypted = encrypted
|
||||
this.plaintext = null
|
||||
}
|
||||
|
||||
constructor(plaintext: Plaintext) : super(1) {
|
||||
this.stream = plaintext.stream
|
||||
this.encrypted = null
|
||||
this.plaintext = plaintext
|
||||
}
|
||||
|
||||
override val type: ObjectType = ObjectType.MSG
|
||||
|
||||
override val isSigned: Boolean = true
|
||||
|
||||
override fun writeBytesToSign(out: OutputStream) {
|
||||
plaintext?.write(out, false) ?: throw IllegalStateException("no plaintext data available")
|
||||
}
|
||||
|
||||
override var signature: ByteArray?
|
||||
get() = plaintext?.signature
|
||||
set(signature) {
|
||||
plaintext?.signature = signature ?: throw IllegalStateException("no plaintext data available")
|
||||
}
|
||||
|
||||
override fun encrypt(publicKey: ByteArray) {
|
||||
this.encrypted = CryptoBox(plaintext ?: throw IllegalStateException("no plaintext data available"), publicKey)
|
||||
}
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
override fun decrypt(privateKey: ByteArray) {
|
||||
plaintext = Plaintext.read(MSG, encrypted!!.decrypt(privateKey))
|
||||
}
|
||||
|
||||
override val isDecrypted: Boolean
|
||||
get() = plaintext != null
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
encrypted?.write(out) ?: throw IllegalStateException("Msg must be signed and encrypted before writing it.")
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
encrypted?.write(buffer) ?: throw IllegalStateException("Msg must be signed and encrypted before writing it.")
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Msg) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
return stream == other.stream && (encrypted == other.encrypted || plaintext == other.plaintext)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return stream.toInt()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACK_LENGTH = 32
|
||||
|
||||
@JvmStatic fun read(`in`: InputStream, stream: Long, length: Int): Msg {
|
||||
return Msg(stream, CryptoBox.read(`in`, length))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Streamable
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* The payload of an 'object' command. This is shared by the network.
|
||||
*/
|
||||
abstract class ObjectPayload protected constructor(val version: Long) : Streamable {
|
||||
|
||||
abstract val type: ObjectType?
|
||||
|
||||
abstract val stream: Long
|
||||
|
||||
open val isSigned: Boolean = false
|
||||
|
||||
open fun writeBytesToSign(out: OutputStream) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ECDSA signature which, as of protocol v3, covers the object header starting with the time,
|
||||
* * appended with the data described in this table down to the extra_bytes. Therefore, this must
|
||||
* * be checked and set in the [ObjectMessage] object.
|
||||
*/
|
||||
open var signature: ByteArray? = null
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
/**
|
||||
* Known types for 'object' messages. Must not be used where an unknown type must be resent.
|
||||
*/
|
||||
enum class ObjectType constructor(val number: Long) {
|
||||
GET_PUBKEY(0),
|
||||
PUBKEY(1),
|
||||
MSG(2),
|
||||
BROADCAST(3);
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun fromNumber(number: Long): ObjectType? {
|
||||
return values().firstOrNull { it.number == number }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Public keys for signing and encryption, the answer to a 'getpubkey' request.
|
||||
*/
|
||||
abstract class Pubkey protected constructor(version: Long) : ObjectPayload(version) {
|
||||
|
||||
override val type: ObjectType = ObjectType.PUBKEY
|
||||
|
||||
abstract val signingKey: ByteArray
|
||||
|
||||
abstract val encryptionKey: ByteArray
|
||||
|
||||
abstract val behaviorBitfield: Int
|
||||
|
||||
val ripe: ByteArray by lazy { cryptography().ripemd160(cryptography().sha512(signingKey, encryptionKey)) }
|
||||
|
||||
open val nonceTrialsPerByte: Long = NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
|
||||
open val extraBytes: Long = NETWORK_EXTRA_BYTES
|
||||
|
||||
open fun writeUnencrypted(out: OutputStream) {
|
||||
write(out)
|
||||
}
|
||||
|
||||
open fun writeUnencrypted(buffer: ByteBuffer) {
|
||||
write(buffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bits 0 through 29 are yet undefined
|
||||
*/
|
||||
enum class Feature constructor(bitNumber: Int) {
|
||||
/**
|
||||
* Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg
|
||||
* messages bound for them.
|
||||
*/
|
||||
INCLUDE_DESTINATION(30),
|
||||
/**
|
||||
* If true, the receiving node does send acknowledgements (rather than dropping them).
|
||||
*/
|
||||
DOES_ACK(31);
|
||||
|
||||
// The Bitmessage Protocol Specification starts counting at the most significant bit,
|
||||
// thus the slightly awkward calculation.
|
||||
// https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features
|
||||
private val bit: Int = 1 shl 31 - bitNumber
|
||||
|
||||
fun isActive(bitfield: Int): Boolean {
|
||||
return bitfield and bit != 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun bitfield(vararg features: Feature): Int {
|
||||
var bits = 0
|
||||
for (feature in features) {
|
||||
bits = bits or feature.bit
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
@JvmStatic fun features(bitfield: Int): Array<Feature> {
|
||||
val features = ArrayList<Feature>(Feature.values().size)
|
||||
for (feature in Feature.values()) {
|
||||
if (bitfield and feature.bit != 0) {
|
||||
features.add(feature)
|
||||
}
|
||||
}
|
||||
return features.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField val LATEST_VERSION: Long = 4
|
||||
|
||||
fun getRipe(publicSigningKey: ByteArray, publicEncryptionKey: ByteArray): ByteArray {
|
||||
return cryptography().ripemd160(cryptography().sha512(publicSigningKey, publicEncryptionKey))
|
||||
}
|
||||
|
||||
fun add0x04(key: ByteArray): ByteArray {
|
||||
if (key.size == 65) return key
|
||||
val result = ByteArray(65)
|
||||
result[0] = 4
|
||||
System.arraycopy(key, 0, result, 1, 64)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* A version 2 public key.
|
||||
*/
|
||||
open class V2Pubkey constructor(version: Long, override val stream: Long, override val behaviorBitfield: Int, signingKey: ByteArray, encryptionKey: ByteArray) : Pubkey(version) {
|
||||
|
||||
override val signingKey: ByteArray = if (signingKey.size == 64) add0x04(signingKey) else signingKey
|
||||
override val encryptionKey: ByteArray = if (encryptionKey.size == 64) add0x04(encryptionKey) else encryptionKey
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
Encode.int32(behaviorBitfield, out)
|
||||
out.write(signingKey, 1, 64)
|
||||
out.write(encryptionKey, 1, 64)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
Encode.int32(behaviorBitfield, buffer)
|
||||
buffer.put(signingKey, 1, 64)
|
||||
buffer.put(encryptionKey, 1, 64)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
internal var streamNumber: Long = 0
|
||||
internal var behaviorBitfield: Int = 0
|
||||
internal var publicSigningKey: ByteArray? = null
|
||||
internal var publicEncryptionKey: ByteArray? = null
|
||||
|
||||
fun stream(streamNumber: Long): Builder {
|
||||
this.streamNumber = streamNumber
|
||||
return this
|
||||
}
|
||||
|
||||
fun behaviorBitfield(behaviorBitfield: Int): Builder {
|
||||
this.behaviorBitfield = behaviorBitfield
|
||||
return this
|
||||
}
|
||||
|
||||
fun publicSigningKey(publicSigningKey: ByteArray): Builder {
|
||||
this.publicSigningKey = publicSigningKey
|
||||
return this
|
||||
}
|
||||
|
||||
fun publicEncryptionKey(publicEncryptionKey: ByteArray): Builder {
|
||||
this.publicEncryptionKey = publicEncryptionKey
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): V2Pubkey {
|
||||
return V2Pubkey(
|
||||
version = 2,
|
||||
stream = streamNumber,
|
||||
behaviorBitfield = behaviorBitfield,
|
||||
signingKey = add0x04(publicSigningKey!!),
|
||||
encryptionKey = add0x04(publicEncryptionKey!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(`in`: InputStream, stream: Long): V2Pubkey {
|
||||
return V2Pubkey(
|
||||
version = 2,
|
||||
stream = stream,
|
||||
behaviorBitfield = Decode.uint32(`in`).toInt(),
|
||||
signingKey = Decode.bytes(`in`, 64),
|
||||
encryptionKey = Decode.bytes(`in`, 64)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A version 3 public key.
|
||||
*/
|
||||
class V3Pubkey protected constructor(
|
||||
version: Long, stream: Long, behaviorBitfield: Int,
|
||||
signingKey: ByteArray, encryptionKey: ByteArray,
|
||||
override val nonceTrialsPerByte: Long,
|
||||
override val extraBytes: Long,
|
||||
override var signature: ByteArray? = null
|
||||
) : V2Pubkey(version, stream, behaviorBitfield, signingKey, encryptionKey) {
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
writeBytesToSign(out)
|
||||
Encode.varBytes(
|
||||
signature ?: throw IllegalStateException("signature not available"),
|
||||
out
|
||||
)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
super.write(buffer)
|
||||
Encode.varInt(nonceTrialsPerByte, buffer)
|
||||
Encode.varInt(extraBytes, buffer)
|
||||
Encode.varBytes(
|
||||
signature ?: throw IllegalStateException("signature not available"),
|
||||
buffer
|
||||
)
|
||||
}
|
||||
|
||||
override val isSigned: Boolean = true
|
||||
|
||||
override fun writeBytesToSign(out: OutputStream) {
|
||||
super.write(out)
|
||||
Encode.varInt(nonceTrialsPerByte, out)
|
||||
Encode.varInt(extraBytes, out)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is V3Pubkey) return false
|
||||
return nonceTrialsPerByte == other.nonceTrialsPerByte &&
|
||||
extraBytes == other.extraBytes &&
|
||||
stream == other.stream &&
|
||||
behaviorBitfield == other.behaviorBitfield &&
|
||||
Arrays.equals(signingKey, other.signingKey) &&
|
||||
Arrays.equals(encryptionKey, other.encryptionKey)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hash(nonceTrialsPerByte, extraBytes)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var streamNumber: Long = 0
|
||||
private var behaviorBitfield: Int = 0
|
||||
private var publicSigningKey: ByteArray? = null
|
||||
private var publicEncryptionKey: ByteArray? = null
|
||||
private var nonceTrialsPerByte: Long = 0
|
||||
private var extraBytes: Long = 0
|
||||
private var signature = ByteArray(0)
|
||||
|
||||
fun stream(streamNumber: Long): Builder {
|
||||
this.streamNumber = streamNumber
|
||||
return this
|
||||
}
|
||||
|
||||
fun behaviorBitfield(behaviorBitfield: Int): Builder {
|
||||
this.behaviorBitfield = behaviorBitfield
|
||||
return this
|
||||
}
|
||||
|
||||
fun publicSigningKey(publicSigningKey: ByteArray): Builder {
|
||||
this.publicSigningKey = publicSigningKey
|
||||
return this
|
||||
}
|
||||
|
||||
fun publicEncryptionKey(publicEncryptionKey: ByteArray): Builder {
|
||||
this.publicEncryptionKey = publicEncryptionKey
|
||||
return this
|
||||
}
|
||||
|
||||
fun nonceTrialsPerByte(nonceTrialsPerByte: Long): Builder {
|
||||
this.nonceTrialsPerByte = nonceTrialsPerByte
|
||||
return this
|
||||
}
|
||||
|
||||
fun extraBytes(extraBytes: Long): Builder {
|
||||
this.extraBytes = extraBytes
|
||||
return this
|
||||
}
|
||||
|
||||
fun signature(signature: ByteArray): Builder {
|
||||
this.signature = signature
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): V3Pubkey {
|
||||
return V3Pubkey(
|
||||
version = 3,
|
||||
stream = streamNumber,
|
||||
behaviorBitfield = behaviorBitfield,
|
||||
signingKey = publicSigningKey!!,
|
||||
encryptionKey = publicEncryptionKey!!,
|
||||
nonceTrialsPerByte = nonceTrialsPerByte,
|
||||
extraBytes = extraBytes,
|
||||
signature = signature
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(`is`: InputStream, stream: Long): V3Pubkey {
|
||||
return V3Pubkey(
|
||||
version = 3,
|
||||
stream = stream,
|
||||
behaviorBitfield = Decode.int32(`is`),
|
||||
signingKey = Decode.bytes(`is`, 64),
|
||||
encryptionKey = Decode.bytes(`is`, 64),
|
||||
nonceTrialsPerByte = Decode.varInt(`is`),
|
||||
extraBytes = Decode.varInt(`is`),
|
||||
signature = Decode.varBytes(`is`)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Users who are subscribed to the sending address will see the message appear in their inbox.
|
||||
* Broadcasts are version 4 or 5.
|
||||
*/
|
||||
open class V4Broadcast : Broadcast {
|
||||
|
||||
override val type: ObjectType = ObjectType.BROADCAST
|
||||
|
||||
protected constructor(version: Long, stream: Long, encrypted: CryptoBox?, plaintext: Plaintext?) : super(version, stream, encrypted, plaintext)
|
||||
|
||||
constructor(senderAddress: BitmessageAddress, plaintext: Plaintext) : super(4, senderAddress.stream, null, plaintext) {
|
||||
if (senderAddress.version >= 4)
|
||||
throw IllegalArgumentException("Address version 3 or older expected, but was " + senderAddress.version)
|
||||
}
|
||||
|
||||
|
||||
override fun writeBytesToSign(out: OutputStream) {
|
||||
plaintext?.write(out, false) ?: throw IllegalStateException("no plaintext data available")
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
encrypted?.write(out) ?: throw IllegalStateException("broadcast not encrypted")
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
encrypted?.write(buffer) ?: throw IllegalStateException("broadcast not encrypted")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(`in`: InputStream, stream: Long, length: Int): V4Broadcast {
|
||||
return V4Broadcast(4, stream, CryptoBox.read(`in`, length), null)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Encrypted
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A version 4 public key. When version 4 pubkeys are created, most of the data in the pubkey is encrypted. This is
|
||||
* done in such a way that only someone who has the Bitmessage address which corresponds to a pubkey can decrypt and
|
||||
* use that pubkey. This prevents people from gathering pubkeys sent around the network and using the data from them
|
||||
* to create messages to be used in spam or in flooding attacks.
|
||||
*/
|
||||
class V4Pubkey : Pubkey, Encrypted {
|
||||
|
||||
override val stream: Long
|
||||
val tag: ByteArray
|
||||
private var encrypted: CryptoBox? = null
|
||||
private var decrypted: V3Pubkey? = null
|
||||
|
||||
private constructor(stream: Long, tag: ByteArray, encrypted: CryptoBox) : super(4) {
|
||||
this.stream = stream
|
||||
this.tag = tag
|
||||
this.encrypted = encrypted
|
||||
}
|
||||
|
||||
constructor(decrypted: V3Pubkey) : super(4) {
|
||||
this.stream = decrypted.stream
|
||||
this.decrypted = decrypted
|
||||
this.tag = BitmessageAddress.calculateTag(4, decrypted.stream, decrypted.ripe)
|
||||
}
|
||||
|
||||
override fun encrypt(publicKey: ByteArray) {
|
||||
if (signature == null) throw IllegalStateException("Pubkey must be signed before encryption.")
|
||||
this.encrypted = CryptoBox(decrypted ?: throw IllegalStateException("no plaintext pubkey data available"), publicKey)
|
||||
}
|
||||
|
||||
@Throws(DecryptionFailedException::class)
|
||||
override fun decrypt(privateKey: ByteArray) {
|
||||
decrypted = V3Pubkey.read(encrypted?.decrypt(privateKey) ?: throw IllegalStateException("no encrypted data available"), stream)
|
||||
}
|
||||
|
||||
override val isDecrypted: Boolean
|
||||
get() = decrypted != null
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
out.write(tag)
|
||||
encrypted?.write(out) ?: throw IllegalStateException("pubkey is encrypted")
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
buffer.put(tag)
|
||||
encrypted?.write(buffer) ?: throw IllegalStateException("pubkey is encrypted")
|
||||
}
|
||||
|
||||
override fun writeUnencrypted(out: OutputStream) {
|
||||
decrypted?.write(out) ?: throw IllegalStateException("pubkey is encrypted")
|
||||
}
|
||||
|
||||
override fun writeUnencrypted(buffer: ByteBuffer) {
|
||||
decrypted?.write(buffer) ?: throw IllegalStateException("pubkey is encrypted")
|
||||
}
|
||||
|
||||
override fun writeBytesToSign(out: OutputStream) {
|
||||
out.write(tag)
|
||||
decrypted?.writeBytesToSign(out) ?: throw IllegalStateException("pubkey is encrypted")
|
||||
}
|
||||
|
||||
override val signingKey: ByteArray
|
||||
get() = decrypted?.signingKey ?: throw IllegalStateException("pubkey is encrypted")
|
||||
|
||||
override val encryptionKey: ByteArray
|
||||
get() = decrypted?.encryptionKey ?: throw IllegalStateException("pubkey is encrypted")
|
||||
|
||||
override val behaviorBitfield: Int
|
||||
get() = decrypted?.behaviorBitfield ?: throw IllegalStateException("pubkey is encrypted")
|
||||
|
||||
override var signature: ByteArray?
|
||||
get() = decrypted?.signature
|
||||
set(signature) {
|
||||
decrypted?.signature = signature
|
||||
}
|
||||
|
||||
override val isSigned: Boolean = true
|
||||
|
||||
override val nonceTrialsPerByte: Long
|
||||
get() = decrypted?.nonceTrialsPerByte ?: throw IllegalStateException("pubkey is encrypted")
|
||||
|
||||
override val extraBytes: Long
|
||||
get() = decrypted?.extraBytes ?: throw IllegalStateException("pubkey is encrypted")
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is V4Pubkey) return false
|
||||
|
||||
if (stream != other.stream) return false
|
||||
if (!Arrays.equals(tag, other.tag)) return false
|
||||
return !if (decrypted != null) decrypted != other.decrypted else other.decrypted != null
|
||||
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = (stream xor stream.ushr(32)).toInt()
|
||||
result = 31 * result + Arrays.hashCode(tag)
|
||||
result = 31 * result + if (decrypted != null) decrypted!!.hashCode() else 0
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(`in`: InputStream, stream: Long, length: Int, encrypted: Boolean): V4Pubkey {
|
||||
if (encrypted)
|
||||
return V4Pubkey(stream,
|
||||
Decode.bytes(`in`, 32),
|
||||
CryptoBox.read(`in`, length - 32))
|
||||
else
|
||||
return V4Pubkey(V3Pubkey.read(`in`, stream))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.payload
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Users who are subscribed to the sending address will see the message appear in their inbox.
|
||||
*/
|
||||
class V5Broadcast : V4Broadcast {
|
||||
|
||||
val tag: ByteArray
|
||||
|
||||
private constructor(stream: Long, tag: ByteArray, encrypted: CryptoBox) : super(5, stream, encrypted, null) {
|
||||
this.tag = tag
|
||||
}
|
||||
|
||||
constructor(senderAddress: BitmessageAddress, plaintext: Plaintext) : super(5, senderAddress.stream, null, plaintext) {
|
||||
if (senderAddress.version < 4)
|
||||
throw IllegalArgumentException("Address version 4 (or newer) expected, but was " + senderAddress.version)
|
||||
this.tag = senderAddress.tag ?: throw IllegalStateException("version 4 address without tag")
|
||||
}
|
||||
|
||||
override fun writeBytesToSign(out: OutputStream) {
|
||||
out.write(tag)
|
||||
super.writeBytesToSign(out)
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
out.write(tag)
|
||||
super.write(out)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun read(`is`: InputStream, stream: Long, length: Int): V5Broadcast {
|
||||
return V5Broadcast(stream, Decode.bytes(`is`, 32), CryptoBox.read(`is`, length - 32))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject
|
||||
|
||||
import ch.dissem.msgpack.types.MPMap
|
||||
import ch.dissem.msgpack.types.MPString
|
||||
import ch.dissem.msgpack.types.MPType
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Serializable
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
|
||||
/**
|
||||
* Extended encoding message object.
|
||||
*/
|
||||
data class ExtendedEncoding(val content: ExtendedEncoding.ExtendedType) : Serializable {
|
||||
|
||||
val type: String? = content.type
|
||||
|
||||
fun zip(): ByteArray {
|
||||
ByteArrayOutputStream().use { out ->
|
||||
DeflaterOutputStream(out).use { zipper -> content.pack().pack(zipper) }
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
interface Unpacker<out T : ExtendedType> {
|
||||
val type: String
|
||||
|
||||
fun unpack(map: MPMap<MPString, MPType<*>>): T
|
||||
}
|
||||
|
||||
interface ExtendedType : Serializable {
|
||||
val type: String
|
||||
|
||||
fun pack(): MPMap<MPString, MPType<*>>
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject
|
||||
|
||||
import ch.dissem.bitmessage.entity.Streamable
|
||||
import ch.dissem.bitmessage.utils.Strings
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
data class InventoryVector constructor(
|
||||
/**
|
||||
* Hash of the object
|
||||
*/
|
||||
val hash: ByteArray) : Streamable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is InventoryVector) return false
|
||||
|
||||
return Arrays.equals(hash, other.hash)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Arrays.hashCode(hash)
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
out.write(hash)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
buffer.put(hash)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return Strings.hex(hash)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun fromHash(hash: ByteArray?): InventoryVector? {
|
||||
return InventoryVector(
|
||||
hash ?: return null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject
|
||||
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
data class Label(
|
||||
private val label: String,
|
||||
val type: Label.Type?,
|
||||
/**
|
||||
* RGBA representation for the color.
|
||||
*/
|
||||
var color: Int
|
||||
) : Serializable {
|
||||
|
||||
var id: Any? = null
|
||||
|
||||
override fun toString(): String {
|
||||
return label
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Label) return false
|
||||
return label == other.label
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hash(label)
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
INBOX,
|
||||
BROADCAST,
|
||||
DRAFT,
|
||||
OUTBOX,
|
||||
SENT,
|
||||
UNREAD,
|
||||
TRASH
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject
|
||||
|
||||
import ch.dissem.bitmessage.entity.Streamable
|
||||
import ch.dissem.bitmessage.entity.Version
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import java.io.OutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.net.SocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
fun ip6(inetAddress: InetAddress): ByteArray {
|
||||
val address = inetAddress.address
|
||||
when (address.size) {
|
||||
16 -> {
|
||||
return address
|
||||
}
|
||||
4 -> {
|
||||
val ip6 = ByteArray(16)
|
||||
ip6[10] = 0xff.toByte()
|
||||
ip6[11] = 0xff.toByte()
|
||||
System.arraycopy(address, 0, ip6, 12, 4)
|
||||
return ip6
|
||||
}
|
||||
else -> throw IllegalArgumentException("Weird address " + inetAddress)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node's address. It's written in IPv6 format.
|
||||
*/
|
||||
data class NetworkAddress(
|
||||
var time: Long,
|
||||
|
||||
/**
|
||||
* Stream number for this node
|
||||
*/
|
||||
val stream: Long,
|
||||
|
||||
/**
|
||||
* same service(s) listed in version
|
||||
*/
|
||||
val services: Long,
|
||||
|
||||
/**
|
||||
* IPv6 address. IPv4 addresses are written into the message as a 16 byte IPv4-mapped IPv6 address
|
||||
* (12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of the IPv4 address).
|
||||
*/
|
||||
val IPv6: ByteArray,
|
||||
val port: Int
|
||||
) : Streamable {
|
||||
|
||||
constructor(time: Long, stream: Long, services: Long = 1, socket: Socket)
|
||||
: this(time, stream, services, ip6(socket.inetAddress), socket.port)
|
||||
|
||||
constructor(time: Long, stream: Long, services: Long = 1, inetAddress: InetAddress, port: Int)
|
||||
: this(time, stream, services, ip6(inetAddress), port)
|
||||
|
||||
fun provides(service: Version.Service?): Boolean = service?.isEnabled(services) ?: false
|
||||
|
||||
fun toInetAddress(): InetAddress {
|
||||
return InetAddress.getByAddress(IPv6)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is NetworkAddress) return false
|
||||
|
||||
return port == other.port && Arrays.equals(IPv6, other.IPv6)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = Arrays.hashCode(IPv6)
|
||||
result = 31 * result + port
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "[" + toInetAddress() + "]:" + port
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
write(out, false)
|
||||
}
|
||||
|
||||
fun write(out: OutputStream, light: Boolean) {
|
||||
if (!light) {
|
||||
Encode.int64(time, out)
|
||||
Encode.int32(stream, out)
|
||||
}
|
||||
Encode.int64(services, out)
|
||||
out.write(IPv6)
|
||||
Encode.int16(port, out)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
write(buffer, false)
|
||||
}
|
||||
|
||||
fun write(buffer: ByteBuffer, light: Boolean) {
|
||||
if (!light) {
|
||||
Encode.int64(time, buffer)
|
||||
Encode.int32(stream, buffer)
|
||||
}
|
||||
Encode.int64(services, buffer)
|
||||
buffer.put(IPv6)
|
||||
Encode.int16(port, buffer)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
internal var time: Long? = null
|
||||
internal var stream: Long = 0
|
||||
internal var services: Long = 1
|
||||
internal var ipv6: ByteArray? = null
|
||||
internal var port: Int = 0
|
||||
|
||||
fun time(time: Long): Builder {
|
||||
this.time = time
|
||||
return this
|
||||
}
|
||||
|
||||
fun stream(stream: Long): Builder {
|
||||
this.stream = stream
|
||||
return this
|
||||
}
|
||||
|
||||
fun services(services: Long): Builder {
|
||||
this.services = services
|
||||
return this
|
||||
}
|
||||
|
||||
fun ip(inetAddress: InetAddress): Builder {
|
||||
ipv6 = ip6(inetAddress)
|
||||
return this
|
||||
}
|
||||
|
||||
fun ipv6(ipv6: ByteArray): Builder {
|
||||
this.ipv6 = ipv6
|
||||
return this
|
||||
}
|
||||
|
||||
fun ipv6(p00: Int, p01: Int, p02: Int, p03: Int,
|
||||
p04: Int, p05: Int, p06: Int, p07: Int,
|
||||
p08: Int, p09: Int, p10: Int, p11: Int,
|
||||
p12: Int, p13: Int, p14: Int, p15: Int): Builder {
|
||||
this.ipv6 = byteArrayOf(p00.toByte(), p01.toByte(), p02.toByte(), p03.toByte(), p04.toByte(), p05.toByte(), p06.toByte(), p07.toByte(), p08.toByte(), p09.toByte(), p10.toByte(), p11.toByte(), p12.toByte(), p13.toByte(), p14.toByte(), p15.toByte())
|
||||
return this
|
||||
}
|
||||
|
||||
fun ipv4(p00: Int, p01: Int, p02: Int, p03: Int): Builder {
|
||||
this.ipv6 = byteArrayOf(0.toByte(), 0.toByte(), 0x00.toByte(), 0x00.toByte(), 0.toByte(), 0.toByte(), 0x00.toByte(), 0x00.toByte(), 0.toByte(), 0.toByte(), 0xff.toByte(), 0xff.toByte(), p00.toByte(), p01.toByte(), p02.toByte(), p03.toByte())
|
||||
return this
|
||||
}
|
||||
|
||||
fun port(port: Int): Builder {
|
||||
this.port = port
|
||||
return this
|
||||
}
|
||||
|
||||
fun address(address: SocketAddress): Builder {
|
||||
if (address is InetSocketAddress) {
|
||||
ip(address.address)
|
||||
port(address.port)
|
||||
} else {
|
||||
throw IllegalArgumentException("Unknown type of address: " + address.javaClass)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): NetworkAddress {
|
||||
return NetworkAddress(
|
||||
time ?: UnixTime.now, stream, services, ipv6!!, port
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField val ANY = NetworkAddress(time = 0, stream = 0, services = 0, IPv6 = ByteArray(16), port = 0)
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Streamable
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.utils.Bytes
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying
|
||||
* [Pubkey] object.
|
||||
*/
|
||||
class PrivateKey : Streamable {
|
||||
|
||||
val privateSigningKey: ByteArray
|
||||
val privateEncryptionKey: ByteArray
|
||||
|
||||
val pubkey: Pubkey
|
||||
|
||||
constructor(shorter: Boolean, stream: Long, nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature) {
|
||||
var privSK: ByteArray
|
||||
var pubSK: ByteArray
|
||||
var privEK: ByteArray
|
||||
var pubEK: ByteArray
|
||||
var ripe: ByteArray
|
||||
do {
|
||||
privSK = cryptography().randomBytes(PRIVATE_KEY_SIZE)
|
||||
privEK = cryptography().randomBytes(PRIVATE_KEY_SIZE)
|
||||
pubSK = cryptography().createPublicKey(privSK)
|
||||
pubEK = cryptography().createPublicKey(privEK)
|
||||
ripe = Pubkey.getRipe(pubSK, pubEK)
|
||||
} while (ripe[0].toInt() != 0 || shorter && ripe[1].toInt() != 0)
|
||||
this.privateSigningKey = privSK
|
||||
this.privateEncryptionKey = privEK
|
||||
this.pubkey = cryptography().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey,
|
||||
nonceTrialsPerByte, extraBytes, *features)
|
||||
}
|
||||
|
||||
constructor(privateSigningKey: ByteArray, privateEncryptionKey: ByteArray, pubkey: Pubkey) {
|
||||
this.privateSigningKey = privateSigningKey
|
||||
this.privateEncryptionKey = privateEncryptionKey
|
||||
this.pubkey = pubkey
|
||||
}
|
||||
|
||||
constructor(address: BitmessageAddress, passphrase: String) : this(address.version, address.stream, passphrase)
|
||||
|
||||
constructor(version: Long, stream: Long, passphrase: String) : this(Builder(version, stream, false).seed(passphrase).generate())
|
||||
|
||||
private constructor(builder: Builder) {
|
||||
this.privateSigningKey = builder.privSK!!
|
||||
this.privateEncryptionKey = builder.privEK!!
|
||||
this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK!!, builder.pubEK!!,
|
||||
InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES)
|
||||
}
|
||||
|
||||
private class Builder internal constructor(internal val version: Long, internal val stream: Long, internal val shorter: Boolean) {
|
||||
|
||||
internal var seed: ByteArray? = null
|
||||
internal var nextNonce: Long = 0
|
||||
|
||||
internal var privSK: ByteArray? = null
|
||||
internal var privEK: ByteArray? = null
|
||||
internal var pubSK: ByteArray? = null
|
||||
internal var pubEK: ByteArray? = null
|
||||
|
||||
internal fun seed(passphrase: String): Builder {
|
||||
try {
|
||||
seed = passphrase.toByteArray(charset("UTF-8"))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
internal fun generate(): Builder {
|
||||
var signingKeyNonce = nextNonce
|
||||
var encryptionKeyNonce = nextNonce + 1
|
||||
var ripe: ByteArray
|
||||
do {
|
||||
privEK = Bytes.truncate(cryptography().sha512(seed!!, Encode.varInt(encryptionKeyNonce)), 32)
|
||||
privSK = Bytes.truncate(cryptography().sha512(seed!!, Encode.varInt(signingKeyNonce)), 32)
|
||||
pubSK = cryptography().createPublicKey(privSK!!)
|
||||
pubEK = cryptography().createPublicKey(privEK!!)
|
||||
ripe = cryptography().ripemd160(cryptography().sha512(pubSK!!, pubEK!!))
|
||||
|
||||
signingKeyNonce += 2
|
||||
encryptionKeyNonce += 2
|
||||
} while (ripe[0].toInt() != 0 || shorter && ripe[1].toInt() != 0)
|
||||
nextNonce = signingKeyNonce
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(out: OutputStream) {
|
||||
Encode.varInt(pubkey.version, out)
|
||||
Encode.varInt(pubkey.stream, out)
|
||||
val baos = ByteArrayOutputStream()
|
||||
pubkey.writeUnencrypted(baos)
|
||||
Encode.varInt(baos.size(), out)
|
||||
out.write(baos.toByteArray())
|
||||
Encode.varBytes(privateSigningKey, out)
|
||||
Encode.varBytes(privateEncryptionKey, out)
|
||||
}
|
||||
|
||||
|
||||
override fun write(buffer: ByteBuffer) {
|
||||
Encode.varInt(pubkey.version, buffer)
|
||||
Encode.varInt(pubkey.stream, buffer)
|
||||
try {
|
||||
val baos = ByteArrayOutputStream()
|
||||
pubkey.writeUnencrypted(baos)
|
||||
Encode.varBytes(baos.toByteArray(), buffer)
|
||||
} catch (e: IOException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
|
||||
Encode.varBytes(privateSigningKey, buffer)
|
||||
Encode.varBytes(privateEncryptionKey, buffer)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField val PRIVATE_KEY_SIZE = 32
|
||||
|
||||
@JvmStatic fun deterministic(passphrase: String, numberOfAddresses: Int, version: Long, stream: Long, shorter: Boolean): List<PrivateKey> {
|
||||
val result = ArrayList<PrivateKey>(numberOfAddresses)
|
||||
val builder = Builder(version, stream, shorter).seed(passphrase)
|
||||
for (i in 0..numberOfAddresses - 1) {
|
||||
builder.generate()
|
||||
result.add(PrivateKey(builder))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun read(`is`: InputStream): PrivateKey {
|
||||
val version = Decode.varInt(`is`).toInt()
|
||||
val stream = Decode.varInt(`is`)
|
||||
val len = Decode.varInt(`is`).toInt()
|
||||
val pubkey = Factory.readPubkey(version.toLong(), stream, `is`, len, false) ?: throw ApplicationException("Unknown pubkey version encountered")
|
||||
val signingKey = Decode.varBytes(`is`)
|
||||
val encryptionKey = Decode.varBytes(`is`)
|
||||
return PrivateKey(signingKey, encryptionKey, pubkey)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject.extended
|
||||
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A "file" attachment as used by extended encoding type messages. Could either be an attachment,
|
||||
* or used inline to be used by a HTML message, for example.
|
||||
*/
|
||||
data class Attachment constructor(
|
||||
val name: String,
|
||||
val data: ByteArray,
|
||||
val type: String,
|
||||
val disposition: Disposition
|
||||
) : Serializable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Attachment) return false
|
||||
return name == other.name &&
|
||||
Arrays.equals(data, other.data) &&
|
||||
type == other.type &&
|
||||
disposition == other.disposition
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hash(name, data, type, disposition)
|
||||
}
|
||||
|
||||
enum class Disposition {
|
||||
inline, attachment
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var name: String? = null
|
||||
private var data: ByteArray? = null
|
||||
private var type: String? = null
|
||||
private var disposition: Disposition? = null
|
||||
|
||||
fun name(name: String): Builder {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
fun data(data: ByteArray): Builder {
|
||||
this.data = data
|
||||
return this
|
||||
}
|
||||
|
||||
fun type(type: String): Builder {
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
fun inline(): Builder {
|
||||
this.disposition = Disposition.inline
|
||||
return this
|
||||
}
|
||||
|
||||
fun attachment(): Builder {
|
||||
this.disposition = Disposition.attachment
|
||||
return this
|
||||
}
|
||||
|
||||
fun disposition(disposition: Disposition): Builder {
|
||||
this.disposition = disposition
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Attachment {
|
||||
return Attachment(name!!, data!!, type!!, disposition!!)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject.extended
|
||||
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.utils.Strings.str
|
||||
import ch.dissem.msgpack.types.*
|
||||
import ch.dissem.msgpack.types.Utils.mp
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.net.URLConnection
|
||||
import java.nio.file.Files
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Extended encoding type 'message'. Properties 'parents' and 'files' not yet supported by PyBitmessage, so they might not work
|
||||
* properly with future PyBitmessage implementations.
|
||||
*/
|
||||
data class Message constructor(
|
||||
val subject: String,
|
||||
val body: String,
|
||||
val parents: List<InventoryVector>,
|
||||
val files: List<Attachment>
|
||||
) : ExtendedEncoding.ExtendedType {
|
||||
|
||||
override val type: String = TYPE
|
||||
|
||||
override fun pack(): MPMap<MPString, MPType<*>> {
|
||||
val result = MPMap<MPString, MPType<*>>()
|
||||
result.put(mp(""), mp(TYPE))
|
||||
result.put(mp("subject"), mp(subject))
|
||||
result.put(mp("body"), mp(body))
|
||||
|
||||
if (!files.isEmpty()) {
|
||||
val items = MPArray<MPMap<MPString, MPType<*>>>()
|
||||
result.put(mp("files"), items)
|
||||
for (file in files) {
|
||||
val item = MPMap<MPString, MPType<*>>()
|
||||
item.put(mp("name"), mp(file.name))
|
||||
item.put(mp("data"), mp(*file.data))
|
||||
item.put(mp("type"), mp(file.type))
|
||||
item.put(mp("disposition"), mp(file.disposition.name))
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
if (!parents.isEmpty()) {
|
||||
val items = MPArray<MPBinary>()
|
||||
result.put(mp("parents"), items)
|
||||
for ((hash) in parents) {
|
||||
items.add(mp(*hash))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var subject: String? = null
|
||||
private var body: String? = null
|
||||
private val parents = LinkedList<InventoryVector>()
|
||||
private val files = LinkedList<Attachment>()
|
||||
|
||||
fun subject(subject: String): Builder {
|
||||
this.subject = subject
|
||||
return this
|
||||
}
|
||||
|
||||
fun body(body: String): Builder {
|
||||
this.body = body
|
||||
return this
|
||||
}
|
||||
|
||||
fun addParent(parent: Plaintext?): Builder {
|
||||
if (parent != null) {
|
||||
val iv = parent.inventoryVector
|
||||
if (iv == null) {
|
||||
LOG.debug("Ignored parent without IV")
|
||||
} else {
|
||||
parents.add(iv)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun addParent(iv: InventoryVector?): Builder {
|
||||
if (iv != null) {
|
||||
parents.add(iv)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun addFile(file: File?, disposition: Attachment.Disposition): Builder {
|
||||
if (file != null) {
|
||||
try {
|
||||
files.add(Attachment.Builder()
|
||||
.name(file.name)
|
||||
.disposition(disposition)
|
||||
.type(URLConnection.guessContentTypeFromStream(FileInputStream(file)))
|
||||
.data(Files.readAllBytes(file.toPath()))
|
||||
.build())
|
||||
} catch (e: IOException) {
|
||||
LOG.error(e.message, e)
|
||||
}
|
||||
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun addFile(file: Attachment?): Builder {
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ExtendedEncoding {
|
||||
return ExtendedEncoding(Message(subject!!, body!!, parents, files))
|
||||
}
|
||||
}
|
||||
|
||||
class Unpacker : ExtendedEncoding.Unpacker<Message> {
|
||||
override val type: String = TYPE
|
||||
|
||||
override fun unpack(map: MPMap<MPString, MPType<*>>): Message {
|
||||
val subject = str(map[mp("subject")]) ?: ""
|
||||
val body = str(map[mp("body")]) ?: ""
|
||||
val parents = LinkedList<InventoryVector>()
|
||||
val files = LinkedList<Attachment>()
|
||||
val mpParents = map[mp("parents")] as? MPArray<*>
|
||||
for (parent in mpParents ?: emptyList<MPArray<MPBinary>>()) {
|
||||
parents.add(InventoryVector.fromHash(
|
||||
(parent as? MPBinary)?.value ?: continue
|
||||
) ?: continue)
|
||||
}
|
||||
val mpFiles = map[mp("files")] as? MPArray<*>
|
||||
for (item in mpFiles ?: emptyList<Any>()) {
|
||||
if (item is MPMap<*, *>) {
|
||||
val b = Attachment.Builder()
|
||||
b.name(str(item[mp("name")])!!)
|
||||
b.data(
|
||||
bin(item[mp("data")] ?: continue) ?: continue
|
||||
)
|
||||
b.type(str(item[mp("type")])!!)
|
||||
val disposition = str(item[mp("disposition")])
|
||||
if ("inline" == disposition) {
|
||||
b.inline()
|
||||
} else if ("attachment" == disposition) {
|
||||
b.attachment()
|
||||
}
|
||||
files.add(b.build())
|
||||
}
|
||||
}
|
||||
|
||||
return Message(subject, body, parents, files)
|
||||
}
|
||||
|
||||
private fun bin(data: MPType<*>): ByteArray? {
|
||||
return (data as? MPBinary)?.value
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(Message::class.java)
|
||||
|
||||
val TYPE = "message"
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.entity.valueobject.extended
|
||||
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.utils.Strings.str
|
||||
import ch.dissem.msgpack.types.MPBinary
|
||||
import ch.dissem.msgpack.types.MPMap
|
||||
import ch.dissem.msgpack.types.MPString
|
||||
import ch.dissem.msgpack.types.MPType
|
||||
import ch.dissem.msgpack.types.Utils.mp
|
||||
|
||||
/**
|
||||
* Extended encoding type 'vote'. Specification still outstanding, so this will need some work.
|
||||
*/
|
||||
data class Vote constructor(val msgId: InventoryVector, val vote: String) : ExtendedEncoding.ExtendedType {
|
||||
|
||||
override val type: String = TYPE
|
||||
|
||||
override fun pack(): MPMap<MPString, MPType<*>> {
|
||||
val result = MPMap<MPString, MPType<*>>()
|
||||
result.put(mp(""), mp(TYPE))
|
||||
result.put(mp("msgId"), mp(*msgId.hash))
|
||||
result.put(mp("vote"), mp(vote))
|
||||
return result
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var msgId: InventoryVector? = null
|
||||
private var vote: String? = null
|
||||
|
||||
fun up(message: Plaintext): ExtendedEncoding {
|
||||
msgId = message.inventoryVector
|
||||
vote = "1"
|
||||
return ExtendedEncoding(Vote(msgId!!, vote!!))
|
||||
}
|
||||
|
||||
fun down(message: Plaintext): ExtendedEncoding {
|
||||
msgId = message.inventoryVector
|
||||
vote = "-1"
|
||||
return ExtendedEncoding(Vote(msgId!!, vote!!))
|
||||
}
|
||||
|
||||
fun msgId(iv: InventoryVector): Builder {
|
||||
this.msgId = iv
|
||||
return this
|
||||
}
|
||||
|
||||
fun vote(vote: String): Builder {
|
||||
this.vote = vote
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ExtendedEncoding {
|
||||
return ExtendedEncoding(Vote(msgId!!, vote!!))
|
||||
}
|
||||
}
|
||||
|
||||
class Unpacker : ExtendedEncoding.Unpacker<Vote> {
|
||||
override val type: String
|
||||
get() = TYPE
|
||||
|
||||
override fun unpack(map: MPMap<MPString, MPType<*>>): Vote {
|
||||
val msgId = InventoryVector.fromHash((map[mp("msgId")] as? MPBinary)?.value) ?: throw IllegalArgumentException("data doesn't contain proper msgId")
|
||||
val vote = str(map[mp("vote")]) ?: throw IllegalArgumentException("no vote given")
|
||||
return Vote(msgId, vote)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField val TYPE = "vote"
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.exception
|
||||
|
||||
/**
|
||||
* Indicates an illegal Bitmessage address
|
||||
*/
|
||||
class AddressFormatException(message: String) : RuntimeException(message)
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.exception
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ApplicationException : RuntimeException {
|
||||
|
||||
constructor(cause: Throwable) : super(cause)
|
||||
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.exception
|
||||
|
||||
class DecryptionFailedException : Exception()
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.exception
|
||||
|
||||
import ch.dissem.bitmessage.utils.Strings
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class InsufficientProofOfWorkException(target: ByteArray, hash: ByteArray) : IOException(
|
||||
"Insufficient proof of work: " + Strings.hex(target) + " required, "
|
||||
+ Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved.")
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.exception
|
||||
|
||||
/**
|
||||
* An exception on the node that's severe enough to cause the client to disconnect this node.
|
||||
|
||||
* @author Ch. Basler
|
||||
*/
|
||||
class NodeException(message: String?, cause: Throwable? = null) : RuntimeException(message ?: cause?.message, cause) {
|
||||
constructor(message: String) : this(message, null)
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.factory
|
||||
|
||||
import ch.dissem.bitmessage.constants.Network.HEADER_SIZE
|
||||
import ch.dissem.bitmessage.constants.Network.MAX_PAYLOAD_SIZE
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A pool for [ByteBuffer]s. As they may use up a lot of memory,
|
||||
* they should be reused as efficiently as possible.
|
||||
*/
|
||||
object BufferPool {
|
||||
private val LOG = LoggerFactory.getLogger(BufferPool::class.java)
|
||||
|
||||
private val pools = mapOf(
|
||||
HEADER_SIZE to Stack<ByteBuffer>(),
|
||||
54 to Stack<ByteBuffer>(),
|
||||
1000 to Stack<ByteBuffer>(),
|
||||
60000 to Stack<ByteBuffer>(),
|
||||
MAX_PAYLOAD_SIZE to Stack<ByteBuffer>()
|
||||
)
|
||||
|
||||
@Synchronized fun allocate(capacity: Int): ByteBuffer {
|
||||
val targetSize = getTargetSize(capacity)
|
||||
val pool = pools[targetSize] ?: throw IllegalStateException("No pool for size $targetSize available")
|
||||
if (pool.isEmpty()) {
|
||||
LOG.trace("Creating new buffer of size $targetSize")
|
||||
return ByteBuffer.allocate(targetSize)
|
||||
} else {
|
||||
return pool.pop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer that has the size of the Bitmessage network message header, 24 bytes.
|
||||
|
||||
* @return a buffer of size 24
|
||||
*/
|
||||
@Synchronized fun allocateHeaderBuffer(): ByteBuffer {
|
||||
val pool = pools[HEADER_SIZE]
|
||||
if (pool == null || pool.isEmpty()) {
|
||||
return ByteBuffer.allocate(HEADER_SIZE)
|
||||
} else {
|
||||
return pool.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun deallocate(buffer: ByteBuffer) {
|
||||
buffer.clear()
|
||||
val pool = pools[buffer.capacity()] ?: throw IllegalArgumentException("Illegal buffer capacity ${buffer.capacity()} one of ${pools.keys} expected.")
|
||||
pool.push(buffer)
|
||||
}
|
||||
|
||||
private fun getTargetSize(capacity: Int): Int {
|
||||
for (size in pools.keys) {
|
||||
if (size >= capacity) return size
|
||||
}
|
||||
throw IllegalArgumentException("Requested capacity too large: requested=$capacity; max=$MAX_PAYLOAD_SIZE")
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.factory
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Vote
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.utils.Strings.str
|
||||
import ch.dissem.msgpack.Reader
|
||||
import ch.dissem.msgpack.types.MPMap
|
||||
import ch.dissem.msgpack.types.MPString
|
||||
import ch.dissem.msgpack.types.MPType
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
/**
|
||||
* Factory that creates [ExtendedEncoding] objects from byte arrays. You can register your own types by adding a
|
||||
* [ExtendedEncoding.Unpacker] using [.registerFactory].
|
||||
*/
|
||||
object ExtendedEncodingFactory {
|
||||
|
||||
private val LOG = LoggerFactory.getLogger(ExtendedEncodingFactory::class.java)
|
||||
private val KEY_MESSAGE_TYPE = MPString("")
|
||||
|
||||
private val factories = HashMap<String, ExtendedEncoding.Unpacker<*>>()
|
||||
|
||||
init {
|
||||
registerFactory(Message.Unpacker())
|
||||
registerFactory(Vote.Unpacker())
|
||||
}
|
||||
|
||||
fun registerFactory(factory: ExtendedEncoding.Unpacker<*>) {
|
||||
factories.put(factory.type, factory)
|
||||
}
|
||||
|
||||
|
||||
fun unzip(zippedData: ByteArray): ExtendedEncoding? {
|
||||
try {
|
||||
InflaterInputStream(ByteArrayInputStream(zippedData)).use { unzipper ->
|
||||
val reader = Reader.getInstance()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val map = reader.read(unzipper) as MPMap<MPString, MPType<*>>
|
||||
val messageType = map[KEY_MESSAGE_TYPE]
|
||||
if (messageType == null) {
|
||||
LOG.error("Missing message type")
|
||||
return null
|
||||
}
|
||||
val factory = factories[str(messageType)]
|
||||
return ExtendedEncoding(
|
||||
factory?.unpack(map) ?: return null
|
||||
)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
}
|
||||
}
|
205
core/src/main/kotlin/ch/dissem/bitmessage/factory/Factory.kt
Normal file
205
core/src/main/kotlin/ch/dissem/bitmessage/factory/Factory.kt
Normal file
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.factory
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.NetworkMessage
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.payload.*
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType.*
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.exception.NodeException
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.SocketException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
/**
|
||||
* Creates [NetworkMessage] objects from [InputStreams][InputStream]
|
||||
*/
|
||||
object Factory {
|
||||
private val LOG = LoggerFactory.getLogger(Factory::class.java)
|
||||
|
||||
@Throws(SocketTimeoutException::class)
|
||||
@JvmStatic fun getNetworkMessage(@Suppress("UNUSED_PARAMETER") version: Int, stream: InputStream): NetworkMessage? {
|
||||
try {
|
||||
return V3MessageFactory.read(stream)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is SocketTimeoutException,
|
||||
is NodeException -> throw e
|
||||
is SocketException -> throw NodeException(e.message, e)
|
||||
else -> {
|
||||
LOG.error(e.message, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JvmStatic fun getObjectMessage(@Suppress("UNUSED_PARAMETER") version: Int, stream: InputStream, length: Int): ObjectMessage? {
|
||||
try {
|
||||
return V3MessageFactory.readObject(stream, length)
|
||||
} catch (e: IOException) {
|
||||
LOG.error(e.message, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun createPubkey(version: Long, stream: Long, publicSigningKey: ByteArray, publicEncryptionKey: ByteArray,
|
||||
nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature): Pubkey {
|
||||
return createPubkey(version, stream, publicSigningKey, publicEncryptionKey, nonceTrialsPerByte, extraBytes,
|
||||
Pubkey.Feature.bitfield(*features))
|
||||
}
|
||||
|
||||
@JvmStatic fun createPubkey(version: Long, stream: Long, publicSigningKey: ByteArray, publicEncryptionKey: ByteArray,
|
||||
nonceTrialsPerByte: Long, extraBytes: Long, behaviourBitfield: Int): Pubkey {
|
||||
if (publicSigningKey.size != 64 && publicSigningKey.size != 65)
|
||||
throw IllegalArgumentException("64 bytes signing key expected, but it was "
|
||||
+ publicSigningKey.size + " bytes long.")
|
||||
if (publicEncryptionKey.size != 64 && publicEncryptionKey.size != 65)
|
||||
throw IllegalArgumentException("64 bytes encryption key expected, but it was "
|
||||
+ publicEncryptionKey.size + " bytes long.")
|
||||
|
||||
when (version.toInt()) {
|
||||
2 -> return V2Pubkey.Builder()
|
||||
.stream(stream)
|
||||
.publicSigningKey(publicSigningKey)
|
||||
.publicEncryptionKey(publicEncryptionKey)
|
||||
.behaviorBitfield(behaviourBitfield)
|
||||
.build()
|
||||
3 -> return V3Pubkey.Builder()
|
||||
.stream(stream)
|
||||
.publicSigningKey(publicSigningKey)
|
||||
.publicEncryptionKey(publicEncryptionKey)
|
||||
.behaviorBitfield(behaviourBitfield)
|
||||
.nonceTrialsPerByte(nonceTrialsPerByte)
|
||||
.extraBytes(extraBytes)
|
||||
.build()
|
||||
4 -> return V4Pubkey(
|
||||
V3Pubkey.Builder()
|
||||
.stream(stream)
|
||||
.publicSigningKey(publicSigningKey)
|
||||
.publicEncryptionKey(publicEncryptionKey)
|
||||
.behaviorBitfield(behaviourBitfield)
|
||||
.nonceTrialsPerByte(nonceTrialsPerByte)
|
||||
.extraBytes(extraBytes)
|
||||
.build()
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unexpected pubkey version " + version)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun createIdentityFromPrivateKey(address: String,
|
||||
privateSigningKey: ByteArray, privateEncryptionKey: ByteArray,
|
||||
nonceTrialsPerByte: Long, extraBytes: Long,
|
||||
behaviourBitfield: Int): BitmessageAddress {
|
||||
val temp = BitmessageAddress(address)
|
||||
val privateKey = PrivateKey(privateSigningKey, privateEncryptionKey,
|
||||
createPubkey(temp.version, temp.stream,
|
||||
cryptography().createPublicKey(privateSigningKey),
|
||||
cryptography().createPublicKey(privateEncryptionKey),
|
||||
nonceTrialsPerByte, extraBytes, behaviourBitfield))
|
||||
val result = BitmessageAddress(privateKey)
|
||||
if (result.address != address) {
|
||||
throw IllegalArgumentException("Address not matching private key. Address: " + address
|
||||
+ "; Address derived from private key: " + result.address)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun generatePrivateAddress(shorter: Boolean,
|
||||
stream: Long,
|
||||
vararg features: Pubkey.Feature): BitmessageAddress {
|
||||
return BitmessageAddress(PrivateKey(shorter, stream, 1000, 1000, *features))
|
||||
}
|
||||
|
||||
@JvmStatic fun getObjectPayload(objectType: Long,
|
||||
version: Long,
|
||||
streamNumber: Long,
|
||||
stream: InputStream,
|
||||
length: Int): ObjectPayload {
|
||||
val type = ObjectType.fromNumber(objectType)
|
||||
if (type != null) {
|
||||
when (type) {
|
||||
GET_PUBKEY -> return parseGetPubkey(version, streamNumber, stream, length)
|
||||
PUBKEY -> return parsePubkey(version, streamNumber, stream, length)
|
||||
MSG -> return parseMsg(version, streamNumber, stream, length)
|
||||
BROADCAST -> return parseBroadcast(version, streamNumber, stream, length)
|
||||
}
|
||||
}
|
||||
// fallback: just store the message - we don't really care what it is
|
||||
LOG.trace("Unexpected object type: " + objectType)
|
||||
return GenericPayload.read(version, streamNumber, stream, length)
|
||||
}
|
||||
|
||||
@JvmStatic private fun parseGetPubkey(version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload {
|
||||
return GetPubkey.read(stream, streamNumber, length, version)
|
||||
}
|
||||
|
||||
@JvmStatic fun readPubkey(version: Long, stream: Long, `is`: InputStream, length: Int, encrypted: Boolean): Pubkey? {
|
||||
when (version.toInt()) {
|
||||
2 -> return V2Pubkey.read(`is`, stream)
|
||||
3 -> return V3Pubkey.read(`is`, stream)
|
||||
4 -> return V4Pubkey.read(`is`, stream, length, encrypted)
|
||||
}
|
||||
LOG.debug("Unexpected pubkey version $version, handling as generic payload object")
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic private fun parsePubkey(version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload {
|
||||
val pubkey = readPubkey(version, streamNumber, stream, length, true)
|
||||
return pubkey ?: GenericPayload.read(version, streamNumber, stream, length)
|
||||
}
|
||||
|
||||
@JvmStatic private fun parseMsg(@Suppress("UNUSED_PARAMETER") version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload {
|
||||
return Msg.read(stream, streamNumber, length)
|
||||
}
|
||||
|
||||
@JvmStatic private fun parseBroadcast(version: Long, streamNumber: Long, stream: InputStream, length: Int): ObjectPayload {
|
||||
when (version.toInt()) {
|
||||
4 -> return V4Broadcast.read(stream, streamNumber, length)
|
||||
5 -> return V5Broadcast.read(stream, streamNumber, length)
|
||||
else -> {
|
||||
LOG.debug("Encountered unknown broadcast version " + version)
|
||||
return GenericPayload.read(version, streamNumber, stream, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun getBroadcast(plaintext: Plaintext): Broadcast {
|
||||
val sendingAddress = plaintext.from
|
||||
if (sendingAddress.version < 4) {
|
||||
return V4Broadcast(sendingAddress, plaintext)
|
||||
} else {
|
||||
return V5Broadcast(sendingAddress, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun createAck(from: BitmessageAddress, ackData: ByteArray?, ttl: Long): ObjectMessage? {
|
||||
val ack = GenericPayload(
|
||||
3, from.stream,
|
||||
ackData ?: return null
|
||||
)
|
||||
return ObjectMessage.Builder().objectType(MSG).payload(ack).expiresTime(UnixTime.now + ttl).build()
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.factory
|
||||
|
||||
import ch.dissem.bitmessage.entity.*
|
||||
import ch.dissem.bitmessage.entity.payload.GenericPayload
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectPayload
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.exception.NodeException
|
||||
import ch.dissem.bitmessage.utils.AccessCounter
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import ch.dissem.bitmessage.utils.Strings
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Creates protocol v3 network messages from [InputStreams][InputStream]
|
||||
*/
|
||||
object V3MessageFactory {
|
||||
private val LOG = LoggerFactory.getLogger(V3MessageFactory::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun read(`in`: InputStream): NetworkMessage? {
|
||||
findMagic(`in`)
|
||||
val command = getCommand(`in`)
|
||||
val length = Decode.uint32(`in`).toInt()
|
||||
if (length > 1600003) {
|
||||
throw NodeException("Payload of $length bytes received, no more than 1600003 was expected.")
|
||||
}
|
||||
val checksum = Decode.bytes(`in`, 4)
|
||||
|
||||
val payloadBytes = Decode.bytes(`in`, length)
|
||||
|
||||
if (testChecksum(checksum, payloadBytes)) {
|
||||
val payload = getPayload(command, ByteArrayInputStream(payloadBytes), length)
|
||||
if (payload != null)
|
||||
return NetworkMessage(payload)
|
||||
else
|
||||
return null
|
||||
} else {
|
||||
throw IOException("Checksum failed for message '$command'")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getPayload(command: String, stream: InputStream, length: Int): MessagePayload? {
|
||||
when (command) {
|
||||
"version" -> return parseVersion(stream)
|
||||
"verack" -> return VerAck()
|
||||
"addr" -> return parseAddr(stream)
|
||||
"inv" -> return parseInv(stream)
|
||||
"getdata" -> return parseGetData(stream)
|
||||
"object" -> return readObject(stream, length)
|
||||
"custom" -> return readCustom(stream, length)
|
||||
else -> {
|
||||
LOG.debug("Unknown command: " + command)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readCustom(`in`: InputStream, length: Int): MessagePayload {
|
||||
return CustomMessage.read(`in`, length)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun readObject(`in`: InputStream, length: Int): ObjectMessage {
|
||||
val counter = AccessCounter()
|
||||
val nonce = Decode.bytes(`in`, 8, counter)
|
||||
val expiresTime = Decode.int64(`in`, counter)
|
||||
val objectType = Decode.uint32(`in`, counter)
|
||||
val version = Decode.varInt(`in`, counter)
|
||||
val stream = Decode.varInt(`in`, counter)
|
||||
|
||||
val data = Decode.bytes(`in`, length - counter.length())
|
||||
var payload: ObjectPayload
|
||||
try {
|
||||
val dataStream = ByteArrayInputStream(data)
|
||||
payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.size)
|
||||
} catch (e: Exception) {
|
||||
if (LOG.isTraceEnabled) {
|
||||
LOG.trace("Could not parse object payload - using generic payload instead", e)
|
||||
LOG.trace(Strings.hex(data))
|
||||
}
|
||||
payload = GenericPayload(version, stream, data)
|
||||
}
|
||||
|
||||
return ObjectMessage.Builder()
|
||||
.nonce(nonce)
|
||||
.expiresTime(expiresTime)
|
||||
.objectType(objectType)
|
||||
.stream(stream)
|
||||
.payload(payload)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun parseGetData(stream: InputStream): GetData {
|
||||
val count = Decode.varInt(stream)
|
||||
val inventoryVectors = LinkedList<InventoryVector>()
|
||||
for (i in 0..count - 1) {
|
||||
inventoryVectors.add(parseInventoryVector(stream))
|
||||
}
|
||||
return GetData(inventoryVectors)
|
||||
}
|
||||
|
||||
private fun parseInv(stream: InputStream): Inv {
|
||||
val count = Decode.varInt(stream)
|
||||
val inventoryVectors = LinkedList<InventoryVector>()
|
||||
for (i in 0..count - 1) {
|
||||
inventoryVectors.add(parseInventoryVector(stream))
|
||||
}
|
||||
return Inv(inventoryVectors)
|
||||
}
|
||||
|
||||
private fun parseAddr(stream: InputStream): Addr {
|
||||
val count = Decode.varInt(stream)
|
||||
val networkAddresses = LinkedList<NetworkAddress>()
|
||||
for (i in 0..count - 1) {
|
||||
networkAddresses.add(parseAddress(stream, false))
|
||||
}
|
||||
return Addr(networkAddresses)
|
||||
}
|
||||
|
||||
private fun parseVersion(stream: InputStream): Version {
|
||||
val version = Decode.int32(stream)
|
||||
val services = Decode.int64(stream)
|
||||
val timestamp = Decode.int64(stream)
|
||||
val addrRecv = parseAddress(stream, true)
|
||||
val addrFrom = parseAddress(stream, true)
|
||||
val nonce = Decode.int64(stream)
|
||||
val userAgent = Decode.varString(stream)
|
||||
val streamNumbers = Decode.varIntList(stream)
|
||||
|
||||
return Version.Builder()
|
||||
.version(version)
|
||||
.services(services)
|
||||
.timestamp(timestamp)
|
||||
.addrRecv(addrRecv).addrFrom(addrFrom)
|
||||
.nonce(nonce)
|
||||
.userAgent(userAgent)
|
||||
.streams(*streamNumbers).build()
|
||||
}
|
||||
|
||||
private fun parseInventoryVector(stream: InputStream): InventoryVector {
|
||||
return InventoryVector(Decode.bytes(stream, 32))
|
||||
}
|
||||
|
||||
private fun parseAddress(stream: InputStream, light: Boolean): NetworkAddress {
|
||||
val time: Long
|
||||
val streamNumber: Long
|
||||
if (!light) {
|
||||
time = Decode.int64(stream)
|
||||
streamNumber = Decode.uint32(stream) // This isn't consistent, not sure if this is correct
|
||||
} else {
|
||||
time = 0
|
||||
streamNumber = 0
|
||||
}
|
||||
val services = Decode.int64(stream)
|
||||
val ipv6 = Decode.bytes(stream, 16)
|
||||
val port = Decode.uint16(stream)
|
||||
return NetworkAddress.Builder()
|
||||
.time(time)
|
||||
.stream(streamNumber)
|
||||
.services(services)
|
||||
.ipv6(ipv6)
|
||||
.port(port)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun testChecksum(checksum: ByteArray, payload: ByteArray): Boolean {
|
||||
val payloadChecksum = cryptography().sha512(payload)
|
||||
for (i in checksum.indices) {
|
||||
if (checksum[i] != payloadChecksum[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getCommand(stream: InputStream): String {
|
||||
val bytes = ByteArray(12)
|
||||
var end = bytes.size
|
||||
for (i in bytes.indices) {
|
||||
bytes[i] = stream.read().toByte()
|
||||
if (end == bytes.size) {
|
||||
if (bytes[i].toInt() == 0) end = i
|
||||
} else {
|
||||
if (bytes[i].toInt() != 0) throw IOException("'\\u0000' padding expected for command")
|
||||
}
|
||||
}
|
||||
return String(bytes, 0, end, Charsets.US_ASCII)
|
||||
}
|
||||
|
||||
private fun findMagic(`in`: InputStream) {
|
||||
var pos = 0
|
||||
for (i in 0..1619999) {
|
||||
val b = `in`.read().toByte()
|
||||
if (b == NetworkMessage.MAGIC_BYTES[pos]) {
|
||||
if (pos + 1 == NetworkMessage.MAGIC_BYTES.size) {
|
||||
return
|
||||
}
|
||||
} else if (pos > 0 && b == NetworkMessage.MAGIC_BYTES[0]) {
|
||||
pos = 1
|
||||
} else {
|
||||
pos = 0
|
||||
}
|
||||
pos++
|
||||
}
|
||||
throw NodeException("Failed to find MAGIC bytes in stream")
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.factory
|
||||
|
||||
import ch.dissem.bitmessage.constants.Network.MAX_PAYLOAD_SIZE
|
||||
import ch.dissem.bitmessage.entity.NetworkMessage
|
||||
import ch.dissem.bitmessage.exception.NodeException
|
||||
import ch.dissem.bitmessage.utils.Decode
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Similar to the [V3MessageFactory], but used for NIO buffers which may or may not contain a whole message.
|
||||
*/
|
||||
class V3MessageReader {
|
||||
private var headerBuffer: ByteBuffer? = null
|
||||
private var dataBuffer: ByteBuffer? = null
|
||||
|
||||
private var state: ReaderState? = ReaderState.MAGIC
|
||||
private var command: String? = null
|
||||
private var length: Int = 0
|
||||
private val checksum = ByteArray(4)
|
||||
|
||||
private val messages = LinkedList<NetworkMessage>()
|
||||
|
||||
fun getActiveBuffer(): ByteBuffer {
|
||||
if (state != null && state != ReaderState.DATA) {
|
||||
if (headerBuffer == null) {
|
||||
headerBuffer = BufferPool.allocateHeaderBuffer()
|
||||
}
|
||||
}
|
||||
return if (state == ReaderState.DATA)
|
||||
dataBuffer ?: throw IllegalStateException("data buffer is null")
|
||||
else
|
||||
headerBuffer ?: throw IllegalStateException("header buffer is null")
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (state != ReaderState.DATA) {
|
||||
getActiveBuffer() // in order to initialize
|
||||
headerBuffer?.flip() ?: throw IllegalStateException("header buffer is null")
|
||||
}
|
||||
when (state) {
|
||||
V3MessageReader.ReaderState.MAGIC -> magic(headerBuffer ?: throw IllegalStateException("header buffer is null"))
|
||||
V3MessageReader.ReaderState.HEADER -> header(headerBuffer ?: throw IllegalStateException("header buffer is null"))
|
||||
V3MessageReader.ReaderState.DATA -> data(dataBuffer ?: throw IllegalStateException("data buffer is null"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun magic(headerBuffer: ByteBuffer) {
|
||||
if (!findMagicBytes(headerBuffer)) {
|
||||
headerBuffer.compact()
|
||||
return
|
||||
} else {
|
||||
state = ReaderState.HEADER
|
||||
header(headerBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun header(headerBuffer: ByteBuffer) {
|
||||
if (headerBuffer.remaining() < 20) {
|
||||
headerBuffer.compact()
|
||||
headerBuffer.limit(20)
|
||||
return
|
||||
}
|
||||
command = getCommand(headerBuffer)
|
||||
length = Decode.uint32(headerBuffer).toInt()
|
||||
if (length > MAX_PAYLOAD_SIZE) {
|
||||
throw NodeException("Payload of " + length + " bytes received, no more than " +
|
||||
MAX_PAYLOAD_SIZE + " was expected.")
|
||||
}
|
||||
headerBuffer.get(checksum)
|
||||
state = ReaderState.DATA
|
||||
this.headerBuffer = null
|
||||
BufferPool.deallocate(headerBuffer)
|
||||
val dataBuffer = BufferPool.allocate(length)
|
||||
this.dataBuffer = dataBuffer
|
||||
dataBuffer.clear()
|
||||
dataBuffer.limit(length)
|
||||
data(dataBuffer)
|
||||
}
|
||||
|
||||
private fun data(dataBuffer: ByteBuffer) {
|
||||
if (dataBuffer.position() < length) {
|
||||
return
|
||||
} else {
|
||||
dataBuffer.flip()
|
||||
}
|
||||
if (!testChecksum(dataBuffer)) {
|
||||
state = ReaderState.MAGIC
|
||||
this.dataBuffer = null
|
||||
BufferPool.deallocate(dataBuffer)
|
||||
throw NodeException("Checksum failed for message '$command'")
|
||||
}
|
||||
try {
|
||||
V3MessageFactory.getPayload(
|
||||
command ?: throw IllegalStateException("command is null"),
|
||||
ByteArrayInputStream(dataBuffer.array(),
|
||||
dataBuffer.arrayOffset() + dataBuffer.position(), length),
|
||||
length
|
||||
)?.let { messages.add(NetworkMessage(it)) }
|
||||
} catch (e: IOException) {
|
||||
throw NodeException(e.message)
|
||||
} finally {
|
||||
state = ReaderState.MAGIC
|
||||
this.dataBuffer = null
|
||||
BufferPool.deallocate(dataBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMessages(): MutableList<NetworkMessage> {
|
||||
return messages
|
||||
}
|
||||
|
||||
private fun findMagicBytes(buffer: ByteBuffer): Boolean {
|
||||
var i = 0
|
||||
while (buffer.hasRemaining()) {
|
||||
if (i == 0) {
|
||||
buffer.mark()
|
||||
}
|
||||
if (buffer.get() == NetworkMessage.MAGIC_BYTES[i]) {
|
||||
i++
|
||||
if (i == NetworkMessage.MAGIC_BYTES.size) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
if (i > 0) {
|
||||
buffer.reset()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getCommand(buffer: ByteBuffer): String {
|
||||
val start = buffer.position()
|
||||
var l = 0
|
||||
while (l < 12 && buffer.get().toInt() != 0) l++
|
||||
var i = l + 1
|
||||
while (i < 12) {
|
||||
if (buffer.get().toInt() != 0) throw NodeException("'\\u0000' padding expected for command")
|
||||
i++
|
||||
}
|
||||
return String(buffer.array(), start, l, Charsets.US_ASCII)
|
||||
}
|
||||
|
||||
private fun testChecksum(buffer: ByteBuffer): Boolean {
|
||||
val payloadChecksum = cryptography().sha512(buffer.array(),
|
||||
buffer.arrayOffset() + buffer.position(), length)
|
||||
for (i in checksum.indices) {
|
||||
if (checksum[i] != payloadChecksum[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* De-allocates all buffers. This method should be called iff the reader isn't used anymore, i.e. when its
|
||||
* connection is severed.
|
||||
*/
|
||||
fun cleanup() {
|
||||
state = null
|
||||
headerBuffer?.let { BufferPool.deallocate(it) }
|
||||
dataBuffer?.let { BufferPool.deallocate(it) }
|
||||
}
|
||||
|
||||
private enum class ReaderState {
|
||||
MAGIC, HEADER, DATA
|
||||
}
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.utils.Bytes
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import ch.dissem.bitmessage.utils.max
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Implements everything that isn't directly dependent on either Spongy- or Bouncycastle.
|
||||
*/
|
||||
abstract class AbstractCryptography protected constructor(@JvmField protected val provider: Provider) : Cryptography, InternalContext.ContextHolder {
|
||||
private lateinit var ctx: InternalContext
|
||||
|
||||
@JvmField protected val ALGORITHM_ECDSA = "ECDSA"
|
||||
@JvmField protected val ALGORITHM_ECDSA_SHA1 = "SHA1withECDSA"
|
||||
@JvmField protected val ALGORITHM_EVP_SHA256 = "SHA256withECDSA"
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
ctx = context
|
||||
}
|
||||
|
||||
override fun sha512(data: ByteArray, offset: Int, length: Int): ByteArray {
|
||||
val mda = md("SHA-512")
|
||||
mda.update(data, offset, length)
|
||||
return mda.digest()
|
||||
}
|
||||
|
||||
override fun sha512(vararg data: ByteArray): ByteArray {
|
||||
return hash("SHA-512", *data)
|
||||
}
|
||||
|
||||
override fun doubleSha512(vararg data: ByteArray): ByteArray {
|
||||
val mda = md("SHA-512")
|
||||
for (d in data) {
|
||||
mda.update(d)
|
||||
}
|
||||
return mda.digest(mda.digest())
|
||||
}
|
||||
|
||||
override fun doubleSha512(data: ByteArray, length: Int): ByteArray {
|
||||
val mda = md("SHA-512")
|
||||
mda.update(data, 0, length)
|
||||
return mda.digest(mda.digest())
|
||||
}
|
||||
|
||||
override fun ripemd160(vararg data: ByteArray): ByteArray {
|
||||
return hash("RIPEMD160", *data)
|
||||
}
|
||||
|
||||
override fun doubleSha256(data: ByteArray, length: Int): ByteArray {
|
||||
val mda = md("SHA-256")
|
||||
mda.update(data, 0, length)
|
||||
return mda.digest(mda.digest())
|
||||
}
|
||||
|
||||
override fun sha1(vararg data: ByteArray): ByteArray {
|
||||
return hash("SHA-1", *data)
|
||||
}
|
||||
|
||||
override fun randomBytes(length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
RANDOM.nextBytes(result)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun doProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long,
|
||||
extraBytes: Long, callback: ProofOfWorkEngine.Callback) {
|
||||
|
||||
val initialHash = getInitialHash(objectMessage)
|
||||
|
||||
val target = getProofOfWorkTarget(objectMessage,
|
||||
max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE), max(extraBytes, NETWORK_EXTRA_BYTES))
|
||||
|
||||
ctx.proofOfWorkEngine.calculateNonce(initialHash, target, callback)
|
||||
}
|
||||
|
||||
@Throws(InsufficientProofOfWorkException::class)
|
||||
override fun checkProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) {
|
||||
val target = getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
|
||||
val value = doubleSha512(objectMessage.nonce ?: throw ApplicationException("Object without nonce"), getInitialHash(objectMessage))
|
||||
if (Bytes.lt(target, value, 8)) {
|
||||
throw InsufficientProofOfWorkException(target, value)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(GeneralSecurityException::class)
|
||||
protected fun doSign(data: ByteArray, privKey: java.security.PrivateKey): ByteArray {
|
||||
// TODO: change this to ALGORITHM_EVP_SHA256 once it's generally used in the network
|
||||
val sig = Signature.getInstance(ALGORITHM_ECDSA_SHA1, provider)
|
||||
sig.initSign(privKey)
|
||||
sig.update(data)
|
||||
return sig.sign()
|
||||
}
|
||||
|
||||
|
||||
@Throws(GeneralSecurityException::class)
|
||||
protected fun doCheckSignature(data: ByteArray, signature: ByteArray, publicKey: PublicKey): Boolean {
|
||||
for (algorithm in arrayOf(ALGORITHM_ECDSA_SHA1, ALGORITHM_EVP_SHA256)) {
|
||||
val sig = Signature.getInstance(algorithm, provider)
|
||||
sig.initVerify(publicKey)
|
||||
sig.update(data)
|
||||
if (sig.verify(signature)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getInitialHash(objectMessage: ObjectMessage): ByteArray {
|
||||
return sha512(objectMessage.payloadBytesWithoutNonce)
|
||||
}
|
||||
|
||||
override fun getProofOfWorkTarget(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long): ByteArray {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val nonceTrialsPerByte = if (nonceTrialsPerByte == 0L) NETWORK_NONCE_TRIALS_PER_BYTE else nonceTrialsPerByte
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val extraBytes = if (extraBytes == 0L) NETWORK_EXTRA_BYTES else extraBytes
|
||||
|
||||
val TTL = BigInteger.valueOf(objectMessage.expiresTime - UnixTime.now)
|
||||
val powLength = BigInteger.valueOf(objectMessage.payloadBytesWithoutNonce.size + extraBytes)
|
||||
val denominator = BigInteger.valueOf(nonceTrialsPerByte)
|
||||
.multiply(
|
||||
powLength.add(
|
||||
powLength.multiply(TTL).divide(TWO_POW_16)
|
||||
)
|
||||
)
|
||||
return Bytes.expand(TWO_POW_64.divide(denominator).toByteArray(), 8)
|
||||
}
|
||||
|
||||
private fun hash(algorithm: String, vararg data: ByteArray): ByteArray {
|
||||
val mda = md(algorithm)
|
||||
for (d in data) {
|
||||
mda.update(d)
|
||||
}
|
||||
return mda.digest()
|
||||
}
|
||||
|
||||
private fun md(algorithm: String): MessageDigest {
|
||||
try {
|
||||
return MessageDigest.getInstance(algorithm, provider)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun mac(key_m: ByteArray, data: ByteArray): ByteArray {
|
||||
try {
|
||||
val mac = Mac.getInstance("HmacSHA256", provider)
|
||||
mac.init(SecretKeySpec(key_m, "HmacSHA256"))
|
||||
return mac.doFinal(data)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun createPubkey(version: Long, stream: Long, privateSigningKey: ByteArray, privateEncryptionKey: ByteArray,
|
||||
nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature): Pubkey {
|
||||
return Factory.createPubkey(version, stream,
|
||||
createPublicKey(privateSigningKey),
|
||||
createPublicKey(privateEncryptionKey),
|
||||
nonceTrialsPerByte, extraBytes, *features)
|
||||
}
|
||||
|
||||
override fun keyToBigInt(privateKey: ByteArray): BigInteger {
|
||||
return BigInteger(1, privateKey)
|
||||
}
|
||||
|
||||
override fun randomNonce(): Long {
|
||||
return RANDOM.nextLong()
|
||||
}
|
||||
|
||||
companion object {
|
||||
protected val LOG = LoggerFactory.getLogger(Cryptography::class.java)
|
||||
private val RANDOM = SecureRandom()
|
||||
private val TWO = BigInteger.valueOf(2)
|
||||
private val TWO_POW_64 = TWO.pow(64)
|
||||
private val TWO_POW_16 = TWO.pow(16)
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.utils.SqlStrings.join
|
||||
import ch.dissem.bitmessage.utils.Strings
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import java.util.*
|
||||
|
||||
abstract class AbstractMessageRepository : MessageRepository, InternalContext.ContextHolder {
|
||||
protected lateinit var ctx: InternalContext
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
ctx = context
|
||||
}
|
||||
|
||||
protected fun saveContactIfNecessary(contact: BitmessageAddress?) {
|
||||
contact?.let {
|
||||
val savedAddress = ctx.addressRepository.getAddress(it.address)
|
||||
if (savedAddress == null) {
|
||||
ctx.addressRepository.save(it)
|
||||
} else {
|
||||
if (savedAddress.pubkey == null && it.pubkey != null) {
|
||||
savedAddress.pubkey = it.pubkey
|
||||
ctx.addressRepository.save(savedAddress)
|
||||
}
|
||||
it.alias = savedAddress.alias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMessage(id: Any): Plaintext {
|
||||
if (id is Long) {
|
||||
return single(find("id=" + id)) ?: throw IllegalArgumentException("There is no message with id $id")
|
||||
} else {
|
||||
throw IllegalArgumentException("Long expected for ID")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMessage(iv: InventoryVector): Plaintext? {
|
||||
return single(find("iv=X'" + Strings.hex(iv.hash) + "'"))
|
||||
}
|
||||
|
||||
override fun getMessage(initialHash: ByteArray): Plaintext? {
|
||||
return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'"))
|
||||
}
|
||||
|
||||
override fun getMessageForAck(ackData: ByteArray): Plaintext? {
|
||||
return single(find("ack_data=X'" + Strings.hex(ackData) + "' AND status='" + Plaintext.Status.SENT + "'"))
|
||||
}
|
||||
|
||||
override fun findMessages(label: Label?): List<Plaintext> {
|
||||
if (label == null) {
|
||||
return find("id NOT IN (SELECT message_id FROM Message_Label)")
|
||||
} else {
|
||||
return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")")
|
||||
}
|
||||
}
|
||||
|
||||
override fun findMessages(status: Plaintext.Status, recipient: BitmessageAddress): List<Plaintext> {
|
||||
return find("status='" + status.name + "' AND recipient='" + recipient.address + "'")
|
||||
}
|
||||
|
||||
override fun findMessages(status: Plaintext.Status): List<Plaintext> {
|
||||
return find("status='" + status.name + "'")
|
||||
}
|
||||
|
||||
override fun findMessages(sender: BitmessageAddress): List<Plaintext> {
|
||||
return find("sender='" + sender.address + "'")
|
||||
}
|
||||
|
||||
override fun findMessagesToResend(): List<Plaintext> {
|
||||
return find("status='" + Plaintext.Status.SENT.name + "'" +
|
||||
" AND next_try < " + UnixTime.now)
|
||||
}
|
||||
|
||||
override fun findResponses(parent: Plaintext): List<Plaintext> {
|
||||
if (parent.inventoryVector == null) {
|
||||
return emptyList()
|
||||
}
|
||||
return find("iv IN (SELECT child FROM Message_Parent"
|
||||
+ " WHERE parent=X'" + Strings.hex(parent.inventoryVector!!.hash) + "')")
|
||||
}
|
||||
|
||||
override fun getConversation(conversationId: UUID): List<Plaintext> {
|
||||
return find("conversation=X'" + conversationId.toString().replace("-", "") + "'")
|
||||
}
|
||||
|
||||
override fun getLabels(): List<Label> {
|
||||
return findLabels("1=1")
|
||||
}
|
||||
|
||||
override fun getLabels(vararg types: Label.Type): List<Label> {
|
||||
return findLabels("type IN (" + join(*types) + ")")
|
||||
}
|
||||
|
||||
protected abstract fun findLabels(where: String): List<Label>
|
||||
|
||||
|
||||
protected fun <T> single(collection: Collection<T>): T? {
|
||||
when (collection.size) {
|
||||
0 -> return null
|
||||
1 -> return collection.iterator().next()
|
||||
else -> throw ApplicationException("This shouldn't happen, found " + collection.size +
|
||||
" items, one or none was expected")
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun find(where: String): List<Plaintext>
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
|
||||
interface AddressRepository {
|
||||
/**
|
||||
* Returns a matching BitmessageAddress if there is one with the given ripe or tag, that
|
||||
* has no public key yet. If it doesn't exist or already has a public key, null is returned.
|
||||
|
||||
* @param ripeOrTag Either ripe or tag (depending of address version) of an address with
|
||||
* * missing public key.
|
||||
* *
|
||||
* @return the matching address if there is one without public key, or null otherwise.
|
||||
*/
|
||||
fun findContact(ripeOrTag: ByteArray): BitmessageAddress?
|
||||
|
||||
fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress?
|
||||
|
||||
/**
|
||||
* @return all Bitmessage addresses that belong to this user, i.e. have a private key.
|
||||
*/
|
||||
fun getIdentities(): List<BitmessageAddress>
|
||||
|
||||
/**
|
||||
* @return all subscribed chans.
|
||||
*/
|
||||
fun getChans(): List<BitmessageAddress>
|
||||
|
||||
fun getSubscriptions(): List<BitmessageAddress>
|
||||
|
||||
fun getSubscriptions(broadcastVersion: Long): List<BitmessageAddress>
|
||||
|
||||
/**
|
||||
* @return all Bitmessage addresses that have no private key or are chans.
|
||||
*/
|
||||
fun getContacts(): List<BitmessageAddress>
|
||||
|
||||
/**
|
||||
* Implementations must not delete cryptographic keys if they're not provided by `address`.
|
||||
|
||||
* @param address to save or update
|
||||
*/
|
||||
fun save(address: BitmessageAddress)
|
||||
|
||||
fun remove(address: BitmessageAddress)
|
||||
|
||||
fun getAddress(address: String): BitmessageAddress?
|
||||
}
|
263
core/src/main/kotlin/ch/dissem/bitmessage/ports/Cryptography.kt
Normal file
263
core/src/main/kotlin/ch/dissem/bitmessage/ports/Cryptography.kt
Normal file
@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES
|
||||
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* Provides some methods to help with hashing and encryption. All randoms are created using [SecureRandom],
|
||||
* which should be secure enough.
|
||||
*/
|
||||
interface Cryptography {
|
||||
/**
|
||||
* A helper method to calculate SHA-512 hashes. Please note that a new [MessageDigest] object is created at
|
||||
* each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in
|
||||
* success on the same thread.
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @param offset of the data to be hashed
|
||||
* *
|
||||
* @param length of the data to be hashed
|
||||
* *
|
||||
* @return SHA-512 hash of data within the given range
|
||||
*/
|
||||
fun sha512(data: ByteArray, offset: Int, length: Int): ByteArray
|
||||
|
||||
/**
|
||||
* A helper method to calculate SHA-512 hashes. Please note that a new [MessageDigest] object is created at
|
||||
* each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in
|
||||
* success on the same thread.
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @return SHA-512 hash of data
|
||||
*/
|
||||
fun sha512(vararg data: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* A helper method to calculate doubleSHA-512 hashes. Please note that a new [MessageDigest] object is created
|
||||
* at each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in
|
||||
* success on the same thread.
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @return SHA-512 hash of data
|
||||
*/
|
||||
fun doubleSha512(vararg data: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* A helper method to calculate double SHA-512 hashes. This method allows to only use a part of the available bytes
|
||||
* to use for the hash calculation.
|
||||
*
|
||||
*
|
||||
* Please note that a new [MessageDigest] object is created at each call (to ensure thread safety), so you
|
||||
* shouldn't use this if you need to do many hash calculations in short order on the same thread.
|
||||
*
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @param length number of bytes to be taken into account
|
||||
* *
|
||||
* @return SHA-512 hash of data
|
||||
*/
|
||||
fun doubleSha512(data: ByteArray, length: Int): ByteArray
|
||||
|
||||
/**
|
||||
* A helper method to calculate RIPEMD-160 hashes. Supplying multiple byte arrays has the same result as a
|
||||
* concatenation of all arrays, but might perform better.
|
||||
*
|
||||
*
|
||||
* Please note that a new [MessageDigest] object is created at
|
||||
* each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short
|
||||
* order on the same thread.
|
||||
*
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @return RIPEMD-160 hash of data
|
||||
*/
|
||||
fun ripemd160(vararg data: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* A helper method to calculate double SHA-256 hashes. This method allows to only use a part of the available bytes
|
||||
* to use for the hash calculation.
|
||||
*
|
||||
*
|
||||
* Please note that a new [MessageDigest] object is created at
|
||||
* each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short
|
||||
* order on the same thread.
|
||||
*
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @param length number of bytes to be taken into account
|
||||
* *
|
||||
* @return SHA-256 hash of data
|
||||
*/
|
||||
fun doubleSha256(data: ByteArray, length: Int): ByteArray
|
||||
|
||||
/**
|
||||
* A helper method to calculate SHA-1 hashes. Supplying multiple byte arrays has the same result as a
|
||||
* concatenation of all arrays, but might perform better.
|
||||
*
|
||||
*
|
||||
* Please note that a new [MessageDigest] object is created at
|
||||
* each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in short
|
||||
* order on the same thread.
|
||||
*
|
||||
|
||||
* @param data to get hashed
|
||||
* *
|
||||
* @return SHA hash of data
|
||||
*/
|
||||
fun sha1(vararg data: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* @param length number of bytes to return
|
||||
* *
|
||||
* @return an array of the given size containing random bytes
|
||||
*/
|
||||
fun randomBytes(length: Int): ByteArray
|
||||
|
||||
/**
|
||||
* Calculates the proof of work. This might take a long time, depending on the hardware, message size and time to
|
||||
* live.
|
||||
|
||||
* @param objectMessage to do the proof of work for
|
||||
* *
|
||||
* @param nonceTrialsPerByte difficulty
|
||||
* *
|
||||
* @param extraBytes bytes to add to the object size (makes it more difficult to send small messages)
|
||||
* *
|
||||
* @param callback to handle nonce once it's calculated
|
||||
*/
|
||||
fun doProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long,
|
||||
extraBytes: Long, callback: ProofOfWorkEngine.Callback)
|
||||
|
||||
/**
|
||||
* @param objectMessage to be checked
|
||||
* *
|
||||
* @param nonceTrialsPerByte difficulty
|
||||
* *
|
||||
* @param extraBytes bytes to add to the object size
|
||||
* *
|
||||
* @throws InsufficientProofOfWorkException if proof of work doesn't check out (makes it more difficult to send small messages)
|
||||
*/
|
||||
@Throws(InsufficientProofOfWorkException::class)
|
||||
fun checkProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long)
|
||||
|
||||
fun getInitialHash(objectMessage: ObjectMessage): ByteArray
|
||||
|
||||
fun getProofOfWorkTarget(objectMessage: ObjectMessage, nonceTrialsPerByte: Long = NETWORK_NONCE_TRIALS_PER_BYTE, extraBytes: Long = NETWORK_EXTRA_BYTES): ByteArray
|
||||
|
||||
/**
|
||||
* Calculates the MAC for a message (data)
|
||||
|
||||
* @param key_m the symmetric key used
|
||||
* *
|
||||
* @param data the message data to calculate the MAC for
|
||||
* *
|
||||
* @return the MAC
|
||||
*/
|
||||
fun mac(key_m: ByteArray, data: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* @param encrypt if true, encrypts data, otherwise tries to decrypt it.
|
||||
* *
|
||||
* @param data
|
||||
* *
|
||||
* @param key_e
|
||||
* *
|
||||
* @return
|
||||
*/
|
||||
fun crypt(encrypt: Boolean, data: ByteArray, key_e: ByteArray, initializationVector: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Create a new public key fom given private keys.
|
||||
|
||||
* @param version of the public key / address
|
||||
* *
|
||||
* @param stream of the address
|
||||
* *
|
||||
* @param privateSigningKey private key used for signing
|
||||
* *
|
||||
* @param privateEncryptionKey private key used for encryption
|
||||
* *
|
||||
* @param nonceTrialsPerByte proof of work difficulty
|
||||
* *
|
||||
* @param extraBytes bytes to add for the proof of work (make it harder for small messages)
|
||||
* *
|
||||
* @param features of the address
|
||||
* *
|
||||
* @return a public key object
|
||||
*/
|
||||
fun createPubkey(version: Long, stream: Long, privateSigningKey: ByteArray, privateEncryptionKey: ByteArray,
|
||||
nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature): Pubkey
|
||||
|
||||
/**
|
||||
* @param privateKey private key as byte array
|
||||
* *
|
||||
* @return a public key corresponding to the given private key
|
||||
*/
|
||||
fun createPublicKey(privateKey: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* @param privateKey private key as byte array
|
||||
* *
|
||||
* @return a big integer representation (unsigned) of the given bytes
|
||||
*/
|
||||
fun keyToBigInt(privateKey: ByteArray): BigInteger
|
||||
|
||||
/**
|
||||
* @param data to check
|
||||
* *
|
||||
* @param signature the signature of the message
|
||||
* *
|
||||
* @param pubkey the sender's public key
|
||||
* *
|
||||
* @return true if the signature is valid, false otherwise
|
||||
*/
|
||||
fun isSignatureValid(data: ByteArray, signature: ByteArray, pubkey: Pubkey): Boolean
|
||||
|
||||
/**
|
||||
* Calculate the signature of data, using the given private key.
|
||||
|
||||
* @param data to be signed
|
||||
* *
|
||||
* @param privateKey to be used for signing
|
||||
* *
|
||||
* @return the signature
|
||||
*/
|
||||
fun getSignature(data: ByteArray, privateKey: ch.dissem.bitmessage.entity.valueobject.PrivateKey): ByteArray
|
||||
|
||||
/**
|
||||
* @return a random number of type long
|
||||
*/
|
||||
fun randomNonce(): Long
|
||||
|
||||
fun multiply(k: ByteArray, r: ByteArray): ByteArray
|
||||
|
||||
fun createPoint(x: ByteArray, y: ByteArray): ByteArray
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.CustomMessage
|
||||
import ch.dissem.bitmessage.entity.MessagePayload
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
interface CustomCommandHandler {
|
||||
fun handle(request: CustomMessage): MessagePayload?
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Status.*
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
|
||||
open class DefaultLabeler : Labeler, InternalContext.ContextHolder {
|
||||
private lateinit var ctx: InternalContext
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
ctx = context
|
||||
}
|
||||
|
||||
override fun setLabels(msg: Plaintext) {
|
||||
msg.status = RECEIVED
|
||||
if (msg.type == BROADCAST) {
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD))
|
||||
} else {
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.INBOX, Label.Type.UNREAD))
|
||||
}
|
||||
}
|
||||
|
||||
override fun markAsDraft(msg: Plaintext) {
|
||||
msg.status = DRAFT
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.DRAFT))
|
||||
}
|
||||
|
||||
override fun markAsSending(msg: Plaintext) {
|
||||
if (msg.to != null && msg.to!!.pubkey == null) {
|
||||
msg.status = PUBKEY_REQUESTED
|
||||
} else {
|
||||
msg.status = DOING_PROOF_OF_WORK
|
||||
}
|
||||
msg.removeLabel(Label.Type.DRAFT)
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.OUTBOX))
|
||||
}
|
||||
|
||||
override fun markAsSent(msg: Plaintext) {
|
||||
msg.status = SENT
|
||||
msg.removeLabel(Label.Type.OUTBOX)
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.SENT))
|
||||
}
|
||||
|
||||
override fun markAsAcknowledged(msg: Plaintext) {
|
||||
msg.status = SENT_ACKNOWLEDGED
|
||||
}
|
||||
|
||||
override fun markAsRead(msg: Plaintext) {
|
||||
msg.removeLabel(Label.Type.UNREAD)
|
||||
}
|
||||
|
||||
override fun markAsUnread(msg: Plaintext) {
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.UNREAD))
|
||||
}
|
||||
|
||||
override fun delete(msg: Plaintext) {
|
||||
msg.labels.clear()
|
||||
msg.addLabels(ctx.messageRepository.getLabels(Label.Type.TRASH))
|
||||
}
|
||||
|
||||
override fun archive(msg: Plaintext) {
|
||||
msg.labels.clear()
|
||||
}
|
||||
}
|
55
core/src/main/kotlin/ch/dissem/bitmessage/ports/Inventory.kt
Normal file
55
core/src/main/kotlin/ch/dissem/bitmessage/ports/Inventory.kt
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
|
||||
/**
|
||||
* The Inventory stores and retrieves objects, cleans up outdated objects and can tell which objects are still missing.
|
||||
*/
|
||||
interface Inventory {
|
||||
/**
|
||||
* Returns the IVs of all valid objects we have for the given streams
|
||||
*/
|
||||
fun getInventory(vararg streams: Long): List<InventoryVector>
|
||||
|
||||
/**
|
||||
* Returns the IVs of all objects in the offer that we don't have already. Implementations are allowed to
|
||||
* ignore the streams parameter, but it must be set when calling this method.
|
||||
*/
|
||||
fun getMissing(offer: List<InventoryVector>, vararg streams: Long): List<InventoryVector>
|
||||
|
||||
fun getObject(vector: InventoryVector): ObjectMessage?
|
||||
|
||||
/**
|
||||
* This method is mainly used to search for public keys to newly added addresses or broadcasts from new
|
||||
* subscriptions.
|
||||
*/
|
||||
fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage>
|
||||
|
||||
fun storeObject(objectMessage: ObjectMessage)
|
||||
|
||||
operator fun contains(objectMessage: ObjectMessage): Boolean
|
||||
|
||||
/**
|
||||
* Deletes all objects that expired 5 minutes ago or earlier
|
||||
* (so we don't accidentally request objects we just deleted)
|
||||
*/
|
||||
fun cleanup()
|
||||
}
|
56
core/src/main/kotlin/ch/dissem/bitmessage/ports/Labeler.kt
Normal file
56
core/src/main/kotlin/ch/dissem/bitmessage/ports/Labeler.kt
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
/**
|
||||
* Defines and sets labels. Note that it should also update the status field of a message.
|
||||
* Generally it's highly advised to override the [DefaultLabeler] whenever possible,
|
||||
* instead of directly implementing the interface.
|
||||
*
|
||||
* As the labeler gets called whenever the state of a message changes, it can also be used
|
||||
* as a listener.
|
||||
*/
|
||||
interface Labeler {
|
||||
/**
|
||||
* Sets the labels of a newly received message.
|
||||
*
|
||||
* @param msg an unlabeled message or broadcast
|
||||
*/
|
||||
fun setLabels(msg: Plaintext)
|
||||
|
||||
fun markAsDraft(msg: Plaintext)
|
||||
|
||||
/**
|
||||
* It is paramount that this methods marks the [Plaintext] object with status
|
||||
* [Plaintext.Status.PUBKEY_REQUESTED] (see [DefaultLabeler])
|
||||
*/
|
||||
fun markAsSending(msg: Plaintext)
|
||||
|
||||
fun markAsSent(msg: Plaintext)
|
||||
|
||||
fun markAsAcknowledged(msg: Plaintext)
|
||||
|
||||
fun markAsRead(msg: Plaintext)
|
||||
|
||||
fun markAsUnread(msg: Plaintext)
|
||||
|
||||
fun delete(msg: Plaintext)
|
||||
|
||||
fun archive(msg: Plaintext)
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Status
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import java.util.*
|
||||
|
||||
interface MessageRepository {
|
||||
fun getLabels(): List<Label>
|
||||
|
||||
fun getLabels(vararg types: Label.Type): List<Label>
|
||||
|
||||
fun countUnread(label: Label?): Int
|
||||
|
||||
fun getMessage(id: Any): Plaintext
|
||||
|
||||
fun getMessage(iv: InventoryVector): Plaintext?
|
||||
|
||||
fun getMessage(initialHash: ByteArray): Plaintext?
|
||||
|
||||
fun getMessageForAck(ackData: ByteArray): Plaintext?
|
||||
|
||||
/**
|
||||
* @param label to search for
|
||||
* *
|
||||
* @return a distinct list of all conversations that have at least one message with the given label.
|
||||
*/
|
||||
fun findConversations(label: Label?): List<UUID>
|
||||
|
||||
fun findMessages(label: Label?): List<Plaintext>
|
||||
|
||||
fun findMessages(status: Status): List<Plaintext>
|
||||
|
||||
fun findMessages(status: Status, recipient: BitmessageAddress): List<Plaintext>
|
||||
|
||||
fun findMessages(sender: BitmessageAddress): List<Plaintext>
|
||||
|
||||
fun findResponses(parent: Plaintext): List<Plaintext>
|
||||
|
||||
fun findMessagesToResend(): List<Plaintext>
|
||||
|
||||
fun save(message: Plaintext)
|
||||
|
||||
fun remove(message: Plaintext)
|
||||
|
||||
/**
|
||||
* Returns all messages with this conversation ID. The returned messages aren't sorted in any way,
|
||||
* so you may prefer to use [ch.dissem.bitmessage.utils.ConversationService.getConversation]
|
||||
* instead.
|
||||
|
||||
* @param conversationId ID of the requested conversation
|
||||
* *
|
||||
* @return all messages with the given conversation ID
|
||||
*/
|
||||
fun getConversation(conversationId: UUID): Collection<Plaintext>
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.exception.ApplicationException
|
||||
import ch.dissem.bitmessage.utils.Bytes
|
||||
import ch.dissem.bitmessage.utils.Bytes.inc
|
||||
import ch.dissem.bitmessage.utils.ThreadFactoryBuilder.Companion.pool
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* A POW engine using all available CPU cores.
|
||||
*/
|
||||
class MultiThreadedPOWEngine : ProofOfWorkEngine {
|
||||
private val waiterPool = Executors.newSingleThreadExecutor(pool("POW-waiter").daemon().build())
|
||||
private val workerPool = Executors.newCachedThreadPool(pool("POW-worker").daemon().build())
|
||||
|
||||
/**
|
||||
* This method will block until all pending nonce calculations are done, but not wait for its own calculation
|
||||
* to finish.
|
||||
* (This implementation becomes very inefficient if multiple nonce are calculated at the same time.)
|
||||
|
||||
* @param initialHash the SHA-512 hash of the object to send, sans nonce
|
||||
* *
|
||||
* @param target the target, representing an unsigned long
|
||||
* *
|
||||
* @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make
|
||||
*/
|
||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
|
||||
waiterPool.execute({
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
var cores = Runtime.getRuntime().availableProcessors()
|
||||
if (cores > 255) cores = 255
|
||||
LOG.info("Doing POW using $cores cores")
|
||||
val workers = ArrayList<Worker>(cores)
|
||||
for (i in 0..cores - 1) {
|
||||
val w = Worker(cores.toByte(), i, initialHash, target)
|
||||
workers.add(w)
|
||||
}
|
||||
val futures = ArrayList<Future<ByteArray>>(cores)
|
||||
// Doing this in the previous loop might cause a ConcurrentModificationException in the worker
|
||||
// if a worker finds a nonce while new ones are still being added.
|
||||
workers.mapTo(futures) { workerPool.submit(it) }
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
futures.firstOrNull { it.isDone }?.let {
|
||||
callback.onNonceCalculated(initialHash, it.get())
|
||||
LOG.info("Nonce calculated in " + (System.currentTimeMillis() - startTime) / 1000 + " seconds")
|
||||
futures.forEach { it.cancel(true) }
|
||||
return@execute
|
||||
}
|
||||
}
|
||||
LOG.error("POW waiter thread interrupted - this should not happen!")
|
||||
} catch (e: ExecutionException) {
|
||||
LOG.error(e.message, e)
|
||||
} catch (e: InterruptedException) {
|
||||
LOG.error("POW waiter thread interrupted - this should not happen!", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private inner class Worker internal constructor(
|
||||
private val numberOfCores: Byte, core: Int,
|
||||
private val initialHash: ByteArray,
|
||||
private val target: ByteArray
|
||||
) : Callable<ByteArray> {
|
||||
private val mda: MessageDigest
|
||||
private val nonce = ByteArray(8)
|
||||
|
||||
init {
|
||||
this.nonce[7] = core.toByte()
|
||||
try {
|
||||
mda = MessageDigest.getInstance("SHA-512")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
LOG.error(e.message, e)
|
||||
throw ApplicationException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun call(): ByteArray? {
|
||||
do {
|
||||
inc(nonce, numberOfCores)
|
||||
mda.update(nonce)
|
||||
mda.update(initialHash)
|
||||
if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) {
|
||||
return nonce
|
||||
}
|
||||
} while (!Thread.interrupted())
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.CustomMessage
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.utils.Property
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* Handles incoming messages
|
||||
*/
|
||||
interface NetworkHandler {
|
||||
|
||||
/**
|
||||
* Connects to the trusted host, fetches and offers new messages and disconnects afterwards.
|
||||
*
|
||||
*
|
||||
* An implementation should disconnect if either the timeout is reached or the returned thread is interrupted.
|
||||
*
|
||||
*/
|
||||
fun synchronize(server: InetAddress, port: Int, timeoutInSeconds: Long): Future<*>
|
||||
|
||||
/**
|
||||
* Send a custom message to a specific node (that should implement handling for this message type) and returns
|
||||
* the response, which in turn is expected to be a [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
|
||||
|
||||
/**
|
||||
* Start a full network node, accepting incoming connections and relaying objects.
|
||||
*/
|
||||
fun start()
|
||||
|
||||
/**
|
||||
* Stop the full network node.
|
||||
*/
|
||||
fun stop()
|
||||
|
||||
/**
|
||||
* Offer new objects to up to 8 random nodes.
|
||||
*/
|
||||
fun offer(iv: InventoryVector)
|
||||
|
||||
/**
|
||||
* Request each of those objects from a node that knows of the requested object.
|
||||
|
||||
* @param inventoryVectors of the objects to be requested
|
||||
*/
|
||||
fun request(inventoryVectors: MutableCollection<InventoryVector>)
|
||||
|
||||
fun getNetworkStatus(): Property
|
||||
|
||||
val isRunning: Boolean
|
||||
|
||||
interface MessageListener {
|
||||
@Throws(IOException::class)
|
||||
fun receive(objectMessage: ObjectMessage)
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
|
||||
/**
|
||||
* Stores and provides known peers.
|
||||
*/
|
||||
interface NodeRegistry {
|
||||
/**
|
||||
* Removes all known nodes from registry. This should work around connection issues
|
||||
* when there are many invalid nodes in the registry.
|
||||
*/
|
||||
fun clear()
|
||||
|
||||
fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress>
|
||||
|
||||
fun offerAddresses(nodes: List<NetworkAddress>)
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Helper class to kick start node registries.
|
||||
*/
|
||||
object NodeRegistryHelper {
|
||||
private val LOG = LoggerFactory.getLogger(NodeRegistryHelper::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun loadStableNodes(): Map<Long, Set<NetworkAddress>> {
|
||||
javaClass.classLoader.getResourceAsStream("nodes.txt").use { `in` ->
|
||||
val scanner = Scanner(`in`)
|
||||
var stream: Long = 0
|
||||
val result = HashMap<Long, Set<NetworkAddress>>()
|
||||
var streamSet: MutableSet<NetworkAddress>? = null
|
||||
while (scanner.hasNext()) {
|
||||
try {
|
||||
val line = scanner.nextLine().trim { it <= ' ' }
|
||||
if (line.startsWith("[stream")) {
|
||||
stream = java.lang.Long.parseLong(line.substring(8, line.lastIndexOf(']')))
|
||||
streamSet = HashSet<NetworkAddress>()
|
||||
result.put(stream, streamSet)
|
||||
} else if (streamSet != null && !line.isEmpty() && !line.startsWith("#")) {
|
||||
val portIndex = line.lastIndexOf(':')
|
||||
val inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex))
|
||||
val port = Integer.valueOf(line.substring(portIndex + 1))!!
|
||||
inetAddresses.mapTo(streamSet) { NetworkAddress(
|
||||
time = UnixTime.now,
|
||||
stream = stream,
|
||||
inetAddress = it,
|
||||
port = port
|
||||
) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
LOG.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
if (LOG.isDebugEnabled) {
|
||||
for ((key, value) in result) {
|
||||
LOG.debug("Stream " + key + ": loaded " + value.size + " bootstrap nodes.")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
/**
|
||||
* Does the proof of work necessary to send an object.
|
||||
*/
|
||||
interface ProofOfWorkEngine {
|
||||
/**
|
||||
* Returns a nonce, such that the first 8 bytes from sha512(sha512(nonce||initialHash)) represent a unsigned long
|
||||
* smaller than target.
|
||||
|
||||
* @param initialHash the SHA-512 hash of the object to send, sans nonce
|
||||
* *
|
||||
* @param target the target, representing an unsigned long
|
||||
* *
|
||||
* @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make
|
||||
* * sure this is only called once.
|
||||
*/
|
||||
fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: Callback)
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* @param nonce 8 bytes nonce
|
||||
*/
|
||||
fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray)
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
/**
|
||||
* Objects that proof of work is currently being done for.
|
||||
|
||||
* @author Christian Basler
|
||||
*/
|
||||
interface ProofOfWorkRepository {
|
||||
fun getItem(initialHash: ByteArray): Item
|
||||
|
||||
fun getItems(): List<ByteArray>
|
||||
|
||||
fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long)
|
||||
|
||||
fun putObject(item: Item)
|
||||
|
||||
fun removeObject(initialHash: ByteArray)
|
||||
|
||||
data class Item @JvmOverloads constructor(
|
||||
val objectMessage: ObjectMessage,
|
||||
val nonceTrialsPerByte: Long,
|
||||
val extraBytes: Long,
|
||||
// Needed for ACK POW calculation
|
||||
val expirationTime: Long? = 0,
|
||||
val message: Plaintext? = null
|
||||
)
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.ports
|
||||
|
||||
import ch.dissem.bitmessage.utils.Bytes
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one.
|
||||
*
|
||||
* **Warning:** implementations probably depend on POW being asynchronous, that's
|
||||
* another reason not to use this one.
|
||||
*/
|
||||
class SimplePOWEngine : ProofOfWorkEngine {
|
||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
|
||||
val mda = MessageDigest.getInstance("SHA-512")
|
||||
val nonce = ByteArray(8)
|
||||
do {
|
||||
Bytes.inc(nonce)
|
||||
mda.update(nonce)
|
||||
mda.update(initialHash)
|
||||
} while (Bytes.lt(target, mda.digest(mda.digest()), 8))
|
||||
callback.onNonceCalculated(initialHash, nonce)
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* Intended to count the bytes read or written during (de-)serialization.
|
||||
*/
|
||||
class AccessCounter {
|
||||
private var count: Int = 0
|
||||
|
||||
/**
|
||||
* Increases the counter by one.
|
||||
*/
|
||||
private fun inc() {
|
||||
count++
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the counter by length.
|
||||
*/
|
||||
private fun inc(length: Int) {
|
||||
count += length
|
||||
}
|
||||
|
||||
fun length(): Int {
|
||||
return count
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Increases the counter by one, if not null.
|
||||
*/
|
||||
@JvmStatic fun inc(counter: AccessCounter?) {
|
||||
counter?.inc()
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the counter by length, if not null.
|
||||
*/
|
||||
@JvmStatic fun inc(counter: AccessCounter?, length: Int) {
|
||||
counter?.inc(length)
|
||||
}
|
||||
}
|
||||
}
|
161
core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt
Normal file
161
core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import ch.dissem.bitmessage.exception.AddressFormatException
|
||||
import java.util.Arrays.copyOfRange
|
||||
|
||||
/**
|
||||
* Base58 encoder and decoder.
|
||||
|
||||
* @author Christian Basler: I removed some dependencies to the BitcoinJ code so it can be used here more easily.
|
||||
*/
|
||||
object Base58 {
|
||||
private val INDEXES = IntArray(128)
|
||||
private val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray()
|
||||
|
||||
init {
|
||||
for (i in INDEXES.indices) {
|
||||
INDEXES[i] = -1
|
||||
}
|
||||
for (i in ALPHABET.indices) {
|
||||
INDEXES[ALPHABET[i].toInt()] = i
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the given bytes in base58. No checksum is appended.
|
||||
|
||||
* @param data to encode
|
||||
* *
|
||||
* @return base58 encoded input
|
||||
*/
|
||||
@JvmStatic fun encode(data: ByteArray): String {
|
||||
if (data.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val bytes = copyOfRange(data, 0, data.size)
|
||||
// Count leading zeroes.
|
||||
var zeroCount = 0
|
||||
while (zeroCount < bytes.size && bytes[zeroCount].toInt() == 0) {
|
||||
++zeroCount
|
||||
}
|
||||
// The actual encoding.
|
||||
val temp = ByteArray(bytes.size * 2)
|
||||
var j = temp.size
|
||||
|
||||
var startAt = zeroCount
|
||||
while (startAt < bytes.size) {
|
||||
val mod = divmod58(bytes, startAt)
|
||||
if (bytes[startAt].toInt() == 0) {
|
||||
++startAt
|
||||
}
|
||||
temp[--j] = ALPHABET[mod.toInt()].toByte()
|
||||
}
|
||||
|
||||
// Strip extra '1' if there are some after decoding.
|
||||
while (j < temp.size && temp[j] == ALPHABET[0].toByte()) {
|
||||
++j
|
||||
}
|
||||
// Add as many leading '1' as there were leading zeros.
|
||||
while (--zeroCount >= 0) {
|
||||
temp[--j] = ALPHABET[0].toByte()
|
||||
}
|
||||
|
||||
val output = copyOfRange(temp, j, temp.size)
|
||||
return String(output, Charsets.US_ASCII)
|
||||
}
|
||||
|
||||
@Throws(AddressFormatException::class)
|
||||
@JvmStatic fun decode(input: String): ByteArray {
|
||||
if (input.isEmpty()) {
|
||||
return ByteArray(0)
|
||||
}
|
||||
val input58 = ByteArray(input.length)
|
||||
// Transform the String to a base58 byte sequence
|
||||
for (i in 0..input.length - 1) {
|
||||
val c = input[i]
|
||||
|
||||
var digit58 = -1
|
||||
if (c.toInt() < 128) {
|
||||
digit58 = INDEXES[c.toInt()]
|
||||
}
|
||||
if (digit58 < 0) {
|
||||
throw AddressFormatException("Illegal character $c at $i")
|
||||
}
|
||||
|
||||
input58[i] = digit58.toByte()
|
||||
}
|
||||
// Count leading zeroes
|
||||
var zeroCount = 0
|
||||
while (zeroCount < input58.size && input58[zeroCount].toInt() == 0) {
|
||||
++zeroCount
|
||||
}
|
||||
// The encoding
|
||||
val temp = ByteArray(input.length)
|
||||
var j = temp.size
|
||||
|
||||
var startAt = zeroCount
|
||||
while (startAt < input58.size) {
|
||||
val mod = divmod256(input58, startAt)
|
||||
if (input58[startAt].toInt() == 0) {
|
||||
++startAt
|
||||
}
|
||||
|
||||
temp[--j] = mod
|
||||
}
|
||||
// Do no add extra leading zeroes, move j to first non null byte.
|
||||
while (j < temp.size && temp[j].toInt() == 0) {
|
||||
++j
|
||||
}
|
||||
return copyOfRange(temp, j - zeroCount, temp.size)
|
||||
}
|
||||
|
||||
//
|
||||
// number -> number / 58, returns number % 58
|
||||
//
|
||||
private fun divmod58(number: ByteArray, startAt: Int): Byte {
|
||||
var remainder = 0
|
||||
for (i in startAt..number.size - 1) {
|
||||
val digit256 = number[i].toInt() and 0xFF
|
||||
val temp = remainder * 256 + digit256
|
||||
|
||||
number[i] = (temp / 58).toByte()
|
||||
|
||||
remainder = temp % 58
|
||||
}
|
||||
|
||||
return remainder.toByte()
|
||||
}
|
||||
|
||||
//
|
||||
// number -> number / 256, returns number % 256
|
||||
//
|
||||
private fun divmod256(number58: ByteArray, startAt: Int): Byte {
|
||||
var remainder = 0
|
||||
for (i in startAt..number58.size - 1) {
|
||||
val digit58 = number58[i].toInt() and 0xFF
|
||||
val temp = remainder * 58 + digit58
|
||||
|
||||
number58[i] = (temp / 256).toByte()
|
||||
|
||||
remainder = temp % 256
|
||||
}
|
||||
|
||||
return remainder.toByte()
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* Waits for a value within a callback method to be set.
|
||||
*/
|
||||
class CallbackWaiter<T> {
|
||||
private val startTime = System.currentTimeMillis()
|
||||
@Volatile private var isSet: Boolean = false
|
||||
private var _value: T? = null
|
||||
var time: Long = 0
|
||||
private set
|
||||
|
||||
fun setValue(value: T?) {
|
||||
synchronized(this) {
|
||||
this.time = System.currentTimeMillis() - startTime
|
||||
this._value = value
|
||||
this.isSet = true
|
||||
}
|
||||
}
|
||||
|
||||
fun waitForValue(): T? {
|
||||
while (!isSet) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
synchronized(this) {
|
||||
return _value
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import java.util.*
|
||||
|
||||
object Collections {
|
||||
private val RANDOM = Random()
|
||||
|
||||
/**
|
||||
* @param count the number of elements to return (if possible)
|
||||
* *
|
||||
* @param collection the collection to take samples from
|
||||
* *
|
||||
* @return a random subset of the given collection, or a copy of the collection if it's not larger than count. The
|
||||
* * result is by no means securely random, but should be random enough so not the same objects get selected over
|
||||
* * and over again.
|
||||
*/
|
||||
@JvmStatic fun <T> selectRandom(count: Int, collection: Collection<T>): List<T> {
|
||||
val result = ArrayList<T>(count)
|
||||
if (collection.size <= count) {
|
||||
result.addAll(collection)
|
||||
} else {
|
||||
var collectionRest = collection.size.toDouble()
|
||||
var resultRest = count.toDouble()
|
||||
var skipMax = Math.ceil(collectionRest / resultRest).toInt()
|
||||
var skip = RANDOM.nextInt(skipMax)
|
||||
for (item in collection) {
|
||||
collectionRest--
|
||||
if (skip > 0) {
|
||||
skip--
|
||||
} else {
|
||||
result.add(item)
|
||||
resultRest--
|
||||
if (resultRest == 0.0) {
|
||||
break
|
||||
}
|
||||
skipMax = Math.ceil(collectionRest / resultRest).toInt()
|
||||
skip = RANDOM.nextInt(skipMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun <T> selectRandom(collection: Collection<T>): T {
|
||||
var index = RANDOM.nextInt(collection.size)
|
||||
for (item in collection) {
|
||||
if (index == 0) {
|
||||
return item
|
||||
}
|
||||
index--
|
||||
}
|
||||
throw IllegalArgumentException("Empty collection? Size: " + collection.size)
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
import java.util.regex.Pattern
|
||||
import java.util.regex.Pattern.CASE_INSENSITIVE
|
||||
|
||||
/**
|
||||
* Service that helps with conversations.
|
||||
*/
|
||||
class ConversationService(private val messageRepository: MessageRepository) {
|
||||
|
||||
private val SUBJECT_PREFIX = Pattern.compile("^(re|fwd?):", CASE_INSENSITIVE)
|
||||
|
||||
/**
|
||||
* Retrieve the whole conversation from one single message. If the message isn't part
|
||||
* of a conversation, a singleton list containing the given message is returned. Otherwise
|
||||
* it's the same as [.getConversation]
|
||||
|
||||
* @param message
|
||||
* *
|
||||
* @return a list of messages that belong to the same conversation.
|
||||
*/
|
||||
fun getConversation(message: Plaintext): List<Plaintext> {
|
||||
return getConversation(message.conversationId)
|
||||
}
|
||||
|
||||
private fun sorted(collection: Collection<Plaintext>): LinkedList<Plaintext> {
|
||||
val result = LinkedList(collection)
|
||||
Collections.sort(result, Comparator<Plaintext> { o1, o2 ->
|
||||
return@Comparator when {
|
||||
o1.received === o2.received -> 0
|
||||
o1.received == null -> -1
|
||||
o2.received == null -> 1
|
||||
else -> -o1.received.compareTo(o2.received)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
fun getConversation(conversationId: UUID): List<Plaintext> {
|
||||
val messages = sorted(messageRepository.getConversation(conversationId))
|
||||
val map = HashMap<InventoryVector, Plaintext>(messages.size)
|
||||
for (message in messages) {
|
||||
message.inventoryVector?.let {
|
||||
map.put(it, message)
|
||||
}
|
||||
}
|
||||
|
||||
val result = LinkedList<Plaintext>()
|
||||
while (!messages.isEmpty()) {
|
||||
val last = messages.poll()
|
||||
val pos = lastParentPosition(last, result)
|
||||
result.add(pos, last)
|
||||
addAncestors(last, result, messages, map)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getSubject(conversation: List<Plaintext>): String? {
|
||||
if (conversation.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
// TODO: this has room for improvement
|
||||
val subject = conversation[0].subject
|
||||
val matcher = SUBJECT_PREFIX.matcher(subject!!)
|
||||
|
||||
return if (matcher.find()) {
|
||||
subject.substring(matcher.end())
|
||||
} else {
|
||||
subject
|
||||
}.trim { it <= ' ' }
|
||||
}
|
||||
|
||||
private fun lastParentPosition(child: Plaintext, messages: LinkedList<Plaintext>): Int {
|
||||
val plaintextIterator = messages.descendingIterator()
|
||||
var i = 0
|
||||
while (plaintextIterator.hasNext()) {
|
||||
val next = plaintextIterator.next()
|
||||
if (isParent(next, child)) {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
return messages.size - i
|
||||
}
|
||||
|
||||
private fun isParent(item: Plaintext, child: Plaintext): Boolean {
|
||||
return child.parents.firstOrNull { it == item.inventoryVector } != null
|
||||
}
|
||||
|
||||
private fun addAncestors(message: Plaintext, result: LinkedList<Plaintext>, messages: LinkedList<Plaintext>, map: MutableMap<InventoryVector, Plaintext>) {
|
||||
for (parentKey in message.parents) {
|
||||
map.remove(parentKey)?.let {
|
||||
messages.remove(it)
|
||||
result.addFirst(it)
|
||||
addAncestors(it, result, messages, map)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
object DebugUtils {
|
||||
private val LOG = LoggerFactory.getLogger(DebugUtils::class.java)
|
||||
|
||||
@JvmStatic fun saveToFile(objectMessage: ObjectMessage) {
|
||||
try {
|
||||
val f = File(System.getProperty("user.home") + "/jabit.error/" + objectMessage.inventoryVector + ".inv")
|
||||
f.createNewFile()
|
||||
objectMessage.write(FileOutputStream(f))
|
||||
} catch (e: IOException) {
|
||||
LOG.debug(e.message, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JvmStatic fun <K> inc(map: MutableMap<K, Int>, key: K) {
|
||||
val value = map[key]
|
||||
if (value == null) {
|
||||
map.put(key, 1)
|
||||
} else {
|
||||
map.put(key, value + 1)
|
||||
}
|
||||
}
|
||||
}
|
114
core/src/main/kotlin/ch/dissem/bitmessage/utils/Decode.kt
Normal file
114
core/src/main/kotlin/ch/dissem/bitmessage/utils/Decode.kt
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* This class handles decoding simple types from byte stream, according to
|
||||
* https://bitmessage.org/wiki/Protocol_specification#Common_structures
|
||||
*/
|
||||
object Decode {
|
||||
@JvmStatic fun shortVarBytes(`in`: InputStream, counter: AccessCounter): ByteArray {
|
||||
val length = uint16(`in`, counter)
|
||||
return bytes(`in`, length, counter)
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun varBytes(`in`: InputStream, counter: AccessCounter? = null): ByteArray {
|
||||
val length = varInt(`in`, counter).toInt()
|
||||
return bytes(`in`, length, counter)
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun bytes(`in`: InputStream, count: Int, counter: AccessCounter? = null): ByteArray {
|
||||
val result = ByteArray(count)
|
||||
var off = 0
|
||||
while (off < count) {
|
||||
val read = `in`.read(result, off, count - off)
|
||||
if (read < 0) {
|
||||
throw IOException("Unexpected end of stream, wanted to read $count bytes but only got $off")
|
||||
}
|
||||
off += read
|
||||
}
|
||||
AccessCounter.inc(counter, count)
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic fun varIntList(`in`: InputStream): LongArray {
|
||||
val length = varInt(`in`).toInt()
|
||||
val result = LongArray(length)
|
||||
|
||||
for (i in 0..length - 1) {
|
||||
result[i] = varInt(`in`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun varInt(`in`: InputStream, counter: AccessCounter? = null): Long {
|
||||
val first = `in`.read()
|
||||
AccessCounter.inc(counter)
|
||||
when (first) {
|
||||
0xfd -> return uint16(`in`, counter).toLong()
|
||||
0xfe -> return uint32(`in`, counter)
|
||||
0xff -> return int64(`in`, counter)
|
||||
else -> return first.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun uint8(`in`: InputStream): Int {
|
||||
return `in`.read()
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun uint16(`in`: InputStream, counter: AccessCounter? = null): Int {
|
||||
AccessCounter.inc(counter, 2)
|
||||
return `in`.read() shl 8 or `in`.read()
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun uint32(`in`: InputStream, counter: AccessCounter? = null): Long {
|
||||
AccessCounter.inc(counter, 4)
|
||||
return (`in`.read() shl 24 or (`in`.read() shl 16) or (`in`.read() shl 8) or `in`.read()).toLong()
|
||||
}
|
||||
|
||||
@JvmStatic fun uint32(`in`: ByteBuffer): Long {
|
||||
return (u(`in`.get()) shl 24 or (u(`in`.get()) shl 16) or (u(`in`.get()) shl 8) or u(`in`.get())).toLong()
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun int32(`in`: InputStream, counter: AccessCounter? = null): Int {
|
||||
AccessCounter.inc(counter, 4)
|
||||
return ByteBuffer.wrap(bytes(`in`, 4)).int
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun int64(`in`: InputStream, counter: AccessCounter? = null): Long {
|
||||
AccessCounter.inc(counter, 8)
|
||||
return ByteBuffer.wrap(bytes(`in`, 8)).long
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun varString(`in`: InputStream, counter: AccessCounter? = null): String {
|
||||
val length = varInt(`in`, counter).toInt()
|
||||
// technically, it says the length in characters, but I think this one might be correct
|
||||
// otherwise it will get complicated, as we'll need to read UTF-8 char by char...
|
||||
return String(bytes(`in`, length, counter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given byte as if it were unsigned.
|
||||
*/
|
||||
@JvmStatic private fun u(b: Byte): Int {
|
||||
return b.toInt() and 0xFF
|
||||
}
|
||||
}
|
175
core/src/main/kotlin/ch/dissem/bitmessage/utils/Encode.kt
Normal file
175
core/src/main/kotlin/ch/dissem/bitmessage/utils/Encode.kt
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import ch.dissem.bitmessage.entity.Streamable
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* This class handles encoding simple types from byte stream, according to
|
||||
* https://bitmessage.org/wiki/Protocol_specification#Common_structures
|
||||
*/
|
||||
object Encode {
|
||||
@JvmStatic fun varIntList(values: LongArray, stream: OutputStream) {
|
||||
varInt(values.size, stream)
|
||||
for (value in values) {
|
||||
varInt(value, stream)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun varIntList(values: LongArray, buffer: ByteBuffer) {
|
||||
varInt(values.size, buffer)
|
||||
for (value in values) {
|
||||
varInt(value, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun varInt(value: Int, buffer: ByteBuffer) = varInt(value.toLong(), buffer)
|
||||
@JvmStatic fun varInt(value: Long, buffer: ByteBuffer) {
|
||||
if (value < 0) {
|
||||
// This is due to the fact that Java doesn't really support unsigned values.
|
||||
// Please be aware that this might be an error due to a smaller negative value being cast to long.
|
||||
// Normally, negative values shouldn't occur within the protocol, and longs large enough for being
|
||||
// recognized as negatives aren't realistic.
|
||||
buffer.put(0xff.toByte())
|
||||
buffer.putLong(value)
|
||||
} else if (value < 0xfd) {
|
||||
buffer.put(value.toByte())
|
||||
} else if (value <= 0xffffL) {
|
||||
buffer.put(0xfd.toByte())
|
||||
buffer.putShort(value.toShort())
|
||||
} else if (value <= 0xffffffffL) {
|
||||
buffer.put(0xfe.toByte())
|
||||
buffer.putInt(value.toInt())
|
||||
} else {
|
||||
buffer.put(0xff.toByte())
|
||||
buffer.putLong(value)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun varInt(value: Int) = varInt(value.toLong())
|
||||
@JvmStatic fun varInt(value: Long): ByteArray {
|
||||
val buffer = ByteBuffer.allocate(9)
|
||||
varInt(value, buffer)
|
||||
buffer.flip()
|
||||
return Bytes.truncate(buffer.array(), buffer.limit())
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun varInt(value: Int, stream: OutputStream, counter: AccessCounter? = null) = varInt(value.toLong(), stream, counter)
|
||||
@JvmStatic @JvmOverloads fun varInt(value: Long, stream: OutputStream, counter: AccessCounter? = null) {
|
||||
val buffer = ByteBuffer.allocate(9)
|
||||
varInt(value, buffer)
|
||||
buffer.flip()
|
||||
stream.write(buffer.array(), 0, buffer.limit())
|
||||
AccessCounter.inc(counter, buffer.limit())
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun int8(value: Long, stream: OutputStream, counter: AccessCounter? = null) = int8(value.toInt(), stream, counter)
|
||||
@JvmStatic @JvmOverloads fun int8(value: Int, stream: OutputStream, counter: AccessCounter? = null) {
|
||||
stream.write(value)
|
||||
AccessCounter.inc(counter)
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun int16(value: Long, stream: OutputStream, counter: AccessCounter? = null) = int16(value.toShort(), stream, counter)
|
||||
@JvmStatic @JvmOverloads fun int16(value: Int, stream: OutputStream, counter: AccessCounter? = null) = int16(value.toShort(), stream, counter)
|
||||
@JvmStatic @JvmOverloads fun int16(value: Short, stream: OutputStream, counter: AccessCounter? = null) {
|
||||
stream.write(ByteBuffer.allocate(2).putShort(value).array())
|
||||
AccessCounter.inc(counter, 2)
|
||||
}
|
||||
|
||||
@JvmStatic fun int16(value: Long, buffer: ByteBuffer) = int16(value.toShort(), buffer)
|
||||
@JvmStatic fun int16(value: Int, buffer: ByteBuffer) = int16(value.toShort(), buffer)
|
||||
@JvmStatic fun int16(value: Short, buffer: ByteBuffer) {
|
||||
buffer.putShort(value)
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun int32(value: Long, stream: OutputStream, counter: AccessCounter? = null) = int32(value.toInt(), stream, counter)
|
||||
@JvmStatic @JvmOverloads fun int32(value: Int, stream: OutputStream, counter: AccessCounter? = null) {
|
||||
stream.write(ByteBuffer.allocate(4).putInt(value).array())
|
||||
AccessCounter.inc(counter, 4)
|
||||
}
|
||||
|
||||
@JvmStatic fun int32(value: Long, buffer: ByteBuffer) = int32(value.toInt(), buffer)
|
||||
@JvmStatic fun int32(value: Int, buffer: ByteBuffer) {
|
||||
buffer.putInt(value)
|
||||
}
|
||||
|
||||
@JvmStatic @JvmOverloads fun int64(value: Long, stream: OutputStream, counter: AccessCounter? = null) {
|
||||
stream.write(ByteBuffer.allocate(8).putLong(value).array())
|
||||
AccessCounter.inc(counter, 8)
|
||||
}
|
||||
|
||||
@JvmStatic fun int64(value: Long, buffer: ByteBuffer) {
|
||||
buffer.putLong(value)
|
||||
}
|
||||
|
||||
@JvmStatic fun varString(value: String, out: OutputStream) {
|
||||
val bytes = value.toByteArray(charset("utf-8"))
|
||||
// Technically, it says the length in characters, but I think this one might be correct.
|
||||
// It doesn't really matter, as only ASCII characters are being used.
|
||||
// see also Decode#varString()
|
||||
varInt(bytes.size.toLong(), out)
|
||||
out.write(bytes)
|
||||
}
|
||||
|
||||
@JvmStatic fun varString(value: String, buffer: ByteBuffer) {
|
||||
val bytes = value.toByteArray()
|
||||
// Technically, it says the length in characters, but I think this one might be correct.
|
||||
// It doesn't really matter, as only ASCII characters are being used.
|
||||
// see also Decode#varString()
|
||||
buffer.put(varInt(bytes.size.toLong()))
|
||||
buffer.put(bytes)
|
||||
}
|
||||
|
||||
@JvmStatic fun varBytes(data: ByteArray, out: OutputStream) {
|
||||
varInt(data.size.toLong(), out)
|
||||
out.write(data)
|
||||
}
|
||||
|
||||
@JvmStatic fun varBytes(data: ByteArray, buffer: ByteBuffer) {
|
||||
varInt(data.size.toLong(), buffer)
|
||||
buffer.put(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a [Streamable] object and returns the byte array.
|
||||
* @param streamable the object to be serialized
|
||||
* @return an array of bytes representing the given streamable object.
|
||||
*/
|
||||
@JvmStatic fun bytes(streamable: Streamable): ByteArray {
|
||||
val stream = ByteArrayOutputStream()
|
||||
streamable.write(stream)
|
||||
return stream.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param streamable the object to be serialized
|
||||
* @param padding the result will be padded such that its length is a multiple of *padding*
|
||||
* @return the bytes of the given [Streamable] object, 0-padded such that the final length is x*padding.
|
||||
*/
|
||||
@JvmStatic fun bytes(streamable: Streamable, padding: Int): ByteArray {
|
||||
val stream = ByteArrayOutputStream()
|
||||
streamable.write(stream)
|
||||
val offset = padding - stream.size() % padding
|
||||
val length = stream.size() + offset
|
||||
val result = ByteArray(length)
|
||||
stream.write(result, offset, stream.size())
|
||||
return result
|
||||
}
|
||||
}
|
23
core/src/main/kotlin/ch/dissem/bitmessage/utils/Numbers.kt
Normal file
23
core/src/main/kotlin/ch/dissem/bitmessage/utils/Numbers.kt
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:JvmName("Numbers")
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
fun max(a: Long, b: Long): Long {
|
||||
return if (a > b) a else b
|
||||
}
|
36
core/src/main/kotlin/ch/dissem/bitmessage/utils/Points.kt
Normal file
36
core/src/main/kotlin/ch/dissem/bitmessage/utils/Points.kt
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* Helper object to get a point from a public key on a elliptic curve.
|
||||
*/
|
||||
object Points {
|
||||
/**
|
||||
* returns X component of the point represented by public key P
|
||||
*/
|
||||
@JvmStatic fun getX(P: ByteArray): ByteArray {
|
||||
return P.sliceArray(1..(P.size - 1) / 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* returns Y component of the point represented by public key P
|
||||
*/
|
||||
@JvmStatic fun getY(P: ByteArray): ByteArray {
|
||||
return P.sliceArray((P.size - 1) / 2 + 1..P.size - 1)
|
||||
}
|
||||
}
|
69
core/src/main/kotlin/ch/dissem/bitmessage/utils/Property.kt
Normal file
69
core/src/main/kotlin/ch/dissem/bitmessage/utils/Property.kt
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now
|
||||
* used to contain different status information. It is by default displayed in some JSON inspired human readable
|
||||
* notation, but you might only want to rely on the 'human readable' part.
|
||||
*
|
||||
*
|
||||
* If you need a real JSON representation, please add a method `toJson()`.
|
||||
*
|
||||
*/
|
||||
class Property private constructor(val name: String, val value: Any? = null, val properties: Array<Property> = emptyArray()) {
|
||||
|
||||
constructor(name: String, value: Any) : this(name = name, value = value, properties = emptyArray())
|
||||
constructor(name: String, vararg properties: Property) : this(name, null, arrayOf(*properties))
|
||||
constructor(name: String, properties: List<Property>) : this(name, null, properties.toTypedArray())
|
||||
|
||||
/**
|
||||
* Returns the property if available or `null` otherwise.
|
||||
* Subproperties can be requested by submitting the sequence of properties.
|
||||
*/
|
||||
fun getProperty(vararg name: String): Property? {
|
||||
properties
|
||||
.filter { name[0] == it.name }
|
||||
.forEach {
|
||||
if (name.size == 1)
|
||||
return it
|
||||
else
|
||||
return it.getProperty(*name.sliceArray(1..name.size - 1))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return toString("")
|
||||
}
|
||||
|
||||
private fun toString(indentation: String): String {
|
||||
val result = StringBuilder()
|
||||
result.append(indentation).append(name).append(": ")
|
||||
if (value != null || properties.isEmpty()) {
|
||||
result.append(value)
|
||||
}
|
||||
if (properties.isNotEmpty()) {
|
||||
result.append("{\n")
|
||||
for (property in properties) {
|
||||
result.append(property.toString(indentation + " ")).append('\n')
|
||||
}
|
||||
result.append(indentation).append("}")
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
36
core/src/main/kotlin/ch/dissem/bitmessage/utils/Singleton.kt
Normal file
36
core/src/main/kotlin/ch/dissem/bitmessage/utils/Singleton.kt
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import ch.dissem.bitmessage.ports.Cryptography
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
object Singleton {
|
||||
private var cryptography by Delegates.notNull<Cryptography>()
|
||||
|
||||
@Synchronized
|
||||
@JvmStatic fun initialize(cryptography: Cryptography) {
|
||||
Singleton.cryptography = cryptography
|
||||
}
|
||||
|
||||
@JvmStatic fun cryptography(): Cryptography {
|
||||
return cryptography
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
|
||||
object SqlStrings {
|
||||
@JvmStatic fun join(vararg objects: Long): String {
|
||||
return objects.joinToString()
|
||||
}
|
||||
|
||||
@JvmStatic fun join(vararg objects: ByteArray): String {
|
||||
return objects.map { Strings.hex(it) }.joinToString()
|
||||
}
|
||||
|
||||
@JvmStatic fun join(vararg types: ObjectType): String {
|
||||
return types.map { it.number }.joinToString()
|
||||
}
|
||||
|
||||
@JvmStatic fun join(vararg types: Enum<*>): String {
|
||||
return types.map { '\'' + it.name + '\'' }.joinToString()
|
||||
}
|
||||
}
|
40
core/src/main/kotlin/ch/dissem/bitmessage/utils/Strings.kt
Normal file
40
core/src/main/kotlin/ch/dissem/bitmessage/utils/Strings.kt
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* Some utilities to handle strings.
|
||||
* TODO: Probably this should be split in a GUI related and an SQL related utility class.
|
||||
*/
|
||||
object Strings {
|
||||
@JvmStatic fun join(vararg objects: Any): String {
|
||||
return objects.joinToString()
|
||||
}
|
||||
|
||||
@JvmStatic fun hex(bytes: ByteArray): String {
|
||||
return bytes.map { String.format("%02x", it) }.joinToString(separator = "")
|
||||
}
|
||||
|
||||
@JvmStatic fun str(o: Any?): String? {
|
||||
return o?.toString()
|
||||
}
|
||||
|
||||
@JvmName("strNonNull")
|
||||
@JvmStatic fun str(o: Any): String {
|
||||
return o.toString()
|
||||
}
|
||||
}
|
49
core/src/main/kotlin/ch/dissem/bitmessage/utils/TTL.kt
Normal file
49
core/src/main/kotlin/ch/dissem/bitmessage/utils/TTL.kt
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* Stores times to live in seconds for different object types. Usually this shouldn't be messed with, but for tests
|
||||
* it might be a good idea to reduce it to a minimum, and on mobile clients you might want to optimize it as well.
|
||||
|
||||
* @author Christian Basler
|
||||
*/
|
||||
object TTL {
|
||||
@JvmStatic var msg = 2 * UnixTime.DAY
|
||||
@JvmName("msg") get
|
||||
@JvmName("msg") set(msg) {
|
||||
field = validate(msg)
|
||||
}
|
||||
|
||||
@JvmStatic var getpubkey = 2 * UnixTime.DAY
|
||||
@JvmName("getpubkey") get
|
||||
@JvmName("getpubkey") set(getpubkey) {
|
||||
field = validate(getpubkey)
|
||||
}
|
||||
|
||||
@JvmStatic var pubkey = 28 * UnixTime.DAY
|
||||
@JvmName("pubkey") get
|
||||
@JvmName("pubkey") set(pubkey) {
|
||||
field = validate(pubkey)
|
||||
}
|
||||
|
||||
private fun validate(ttl: Long): Long {
|
||||
if (ttl < 0 || ttl > 28 * UnixTime.DAY)
|
||||
throw IllegalArgumentException("TTL must be between 0 seconds and 28 days")
|
||||
return ttl
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class ThreadFactoryBuilder private constructor(pool: String) {
|
||||
private val namePrefix: String = pool + "-thread-"
|
||||
private var prio = Thread.NORM_PRIORITY
|
||||
private var daemon = false
|
||||
|
||||
fun lowPrio(): ThreadFactoryBuilder {
|
||||
prio = Thread.MIN_PRIORITY
|
||||
return this
|
||||
}
|
||||
|
||||
fun daemon(): ThreadFactoryBuilder {
|
||||
daemon = true
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ThreadFactory {
|
||||
val s = System.getSecurityManager()
|
||||
val group = if (s != null)
|
||||
s.threadGroup
|
||||
else
|
||||
Thread.currentThread().threadGroup
|
||||
|
||||
return object : ThreadFactory {
|
||||
private val threadNumber = AtomicInteger(1)
|
||||
|
||||
override fun newThread(r: Runnable): Thread {
|
||||
val t = Thread(group, r,
|
||||
namePrefix + threadNumber.getAndIncrement(),
|
||||
0)
|
||||
t.priority = prio
|
||||
t.isDaemon = daemon
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun pool(name: String): ThreadFactoryBuilder {
|
||||
return ThreadFactoryBuilder(name)
|
||||
}
|
||||
}
|
||||
}
|
43
core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt
Normal file
43
core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2017 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.bitmessage.utils
|
||||
|
||||
/**
|
||||
* A simple utility class that simplifies using the second based time used in Bitmessage.
|
||||
*/
|
||||
object UnixTime {
|
||||
/**
|
||||
* Length of a minute in seconds, intended for use with [.now].
|
||||
*/
|
||||
@JvmField val MINUTE = 60L
|
||||
/**
|
||||
* Length of an hour in seconds, intended for use with [.now].
|
||||
*/
|
||||
@JvmField val HOUR = 60L * MINUTE
|
||||
/**
|
||||
* Length of a day in seconds, intended for use with [.now].
|
||||
*/
|
||||
@JvmField val DAY = 24L * HOUR
|
||||
|
||||
/**
|
||||
* @return the time in second based Unix time ([System.currentTimeMillis]/1000)
|
||||
*/
|
||||
@JvmStatic val now: Long
|
||||
@JvmName("now") get() {
|
||||
return System.currentTimeMillis() / 1000L
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user