From fd08fa3883c65a36dc85ac7a9ea944ca4e49b5f1 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Sun, 16 Jul 2017 21:25:12 +0200 Subject: [PATCH 1/7] Fixed build --- .../ch/dissem/bitmessage/networking/NetworkHandlerTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt index 4eaface..990b31d 100644 --- a/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt +++ b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt @@ -233,7 +233,7 @@ class NetworkHandlerTest { private val peerAddress = NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(6001).build() private fun shutdown(ctx: BitmessageContext) { - if (!ctx.isRunning) return + if (!ctx.isRunning()) return ctx.shutdown() do { @@ -242,7 +242,7 @@ class NetworkHandlerTest { } catch (ignore: InterruptedException) { } - } while (ctx.isRunning) + } while (ctx.isRunning()) } private fun shutdown(networkHandler: NetworkHandler) { From 6c04aa683e9aee3ee16f85d6e3d17eb6a4750bb9 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Fri, 21 Jul 2017 10:27:20 +0200 Subject: [PATCH 2/7] Added exports for messages, labels and adddresses and some code improvements --- build.gradle | 2 + .../ch/dissem/bitmessage/entity/Plaintext.kt | 4 +- .../entity/valueobject/PrivateKey.kt | 90 +++++++++------ .../utils/ConversationServiceTest.kt | 32 +++--- exports/build.gradle | 23 ++++ .../bitmessage/exports/ContactExport.kt | 81 +++++++++++++ .../bitmessage/exports/MessageExport.kt | 106 ++++++++++++++++++ .../bitmessage/exports/ContactExportTest.kt | 73 ++++++++++++ .../bitmessage/exports/MessageExportTest.kt | 89 +++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 4 +- .../networking/NetworkHandlerTest.kt | 1 - settings.gradle | 4 +- 12 files changed, 453 insertions(+), 56 deletions(-) create mode 100644 exports/build.gradle create mode 100644 exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt create mode 100644 exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt create mode 100644 exports/src/test/kotlin/ch/dissem/bitmessage/exports/ContactExportTest.kt create mode 100644 exports/src/test/kotlin/ch/dissem/bitmessage/exports/MessageExportTest.kt diff --git a/build.gradle b/build.gradle index df0b9d4..41f5805 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ subprojects { repositories { mavenCentral() maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + jcenter() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre7" @@ -142,6 +143,7 @@ subprojects { dependency 'com.madgag.spongycastle:prov:1.56.0.0' dependency 'org.apache.commons:commons-lang3:3.6' dependency 'org.flywaydb:flyway-core:4.2.0' + dependency 'com.beust:klaxon:0.31' dependency 'args4j:args4j:2.33' dependency 'org.ini4j:ini4j:0.5.4' diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt index 3f0f551..72506b6 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt @@ -438,7 +438,7 @@ class Plaintext private constructor( if (this === other) return true if (other !is Plaintext) return false return encoding == other.encoding && - from == other.from && + from.address == other.from.address && Arrays.equals(message, other.message) && ackMessage == other.ackMessage && Arrays.equals(to?.ripe, other.to?.ripe) && @@ -655,7 +655,7 @@ class Plaintext private constructor( return this } - fun signature(signature: ByteArray): Builder { + fun signature(signature: ByteArray?): Builder { this.signature = signature return this } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt index cfc618a..c7f0b54 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/PrivateKey.kt @@ -34,48 +34,37 @@ 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 +data class PrivateKey( + val privateSigningKey: ByteArray, + val privateEncryptionKey: ByteArray, val pubkey: Pubkey +) : Streamable { - 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( + shorter: Boolean, + stream: Long, + nonceTrialsPerByte: Long, extraBytes: Long, + vararg features: Pubkey.Feature + ) : this( + Builder(version = Pubkey.LATEST_VERSION, stream = stream, shorter = shorter) + .random() + .nonceTrialsPerByte(nonceTrialsPerByte) + .extraBytes(extraBytes) + .features(features) + .generate()) 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()) + 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 constructor(builder: Builder) : this( + builder.privSK!!, builder.privEK!!, + Factory.createPubkey(builder.version, builder.stream, builder.pubSK!!, builder.pubEK!!, + builder.nonceTrialsPerByte, builder.extraBytes, *builder.features) + ) private class Builder internal constructor(internal val version: Long, internal val stream: Long, internal val shorter: Boolean) { @@ -87,6 +76,30 @@ class PrivateKey : Streamable { internal var pubSK: ByteArray? = null internal var pubEK: ByteArray? = null + internal var nonceTrialsPerByte = InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE + internal var extraBytes = InternalContext.NETWORK_EXTRA_BYTES + internal var features: Array = emptyArray() + + internal fun random(): Builder { + seed = cryptography().randomBytes(1024) + return this + } + + fun nonceTrialsPerByte(nonceTrialsPerByte: Long): Builder { + this.nonceTrialsPerByte = nonceTrialsPerByte + return this + } + + fun extraBytes(extraBytes: Long): Builder { + this.extraBytes = extraBytes + return this + } + + fun features(features: Array): Builder { + this.features = features + return this + } + internal fun seed(passphrase: String): Builder { try { seed = passphrase.toByteArray(charset("UTF-8")) @@ -143,6 +156,13 @@ class PrivateKey : Streamable { Encode.varBytes(privateEncryptionKey, buffer) } + override fun equals(other: Any?) = other is PrivateKey + && Arrays.equals(privateEncryptionKey, other.privateEncryptionKey) + && Arrays.equals(privateSigningKey, other.privateSigningKey) + && pubkey == other.pubkey + + override fun hashCode() = pubkey.hashCode() + companion object { @JvmField val PRIVATE_KEY_SIZE = 32 diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt index cb26e5f..f960086 100644 --- a/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/ConversationServiceTest.kt @@ -36,6 +36,8 @@ class ConversationServiceTest : TestBase() { private val messageRepository = mock() private val conversationService = spy(ConversationService(messageRepository)) + private val conversation = conversation(alice, bob) + @Test fun `ensure conversation is sorted properly`() { MockitoKotlin.registerInstanceCreator { UUID.randomUUID() } @@ -46,8 +48,9 @@ class ConversationServiceTest : TestBase() { assertThat(actual, `is`(expected)) } - private val conversation: List - get() { + companion object { + private var timer = 2 + fun conversation(alice: BitmessageAddress, bob: BitmessageAddress): List<Plaintext> { val result = LinkedList<Plaintext>() val older = plaintext(alice, bob, @@ -99,19 +102,18 @@ class ConversationServiceTest : TestBase() { return result } - private var timer = 2 - - private fun plaintext(from: BitmessageAddress, to: BitmessageAddress, - content: ExtendedEncoding, status: Plaintext.Status): Plaintext { - val builder = Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(from) - .to(to) - .message(content) - .status(status) - if (status !== Plaintext.Status.DRAFT && status !== Plaintext.Status.DOING_PROOF_OF_WORK) { - builder.received(5L * ++timer - RANDOM.nextInt(10)) + fun plaintext(from: BitmessageAddress, to: BitmessageAddress, + content: ExtendedEncoding, status: Plaintext.Status): Plaintext { + val builder = Plaintext.Builder(MSG) + .IV(TestUtils.randomInventoryVector()) + .from(from) + .to(to) + .message(content) + .status(status) + if (status !== Plaintext.Status.DRAFT && status !== Plaintext.Status.DOING_PROOF_OF_WORK) { + builder.received(5L * ++timer - RANDOM.nextInt(10)) + } + return builder.build() } - return builder.build() } } diff --git a/exports/build.gradle b/exports/build.gradle new file mode 100644 index 0000000..718aabb --- /dev/null +++ b/exports/build.gradle @@ -0,0 +1,23 @@ +uploadArchives { + repositories { + mavenDeployer { + pom.project { + name 'Jabit Exports' + artifactId = 'jabit-exports' + description 'Import and export data to JSON files.' + } + } + } +} + +dependencies { + compile project(':core') + compile 'org.slf4j:slf4j-api' + compile 'com.beust:klaxon' + + testCompile 'junit:junit:4.12' + testCompile 'org.hamcrest:hamcrest-library:1.3' + testCompile 'com.nhaarman:mockito-kotlin:1.5.0' + testCompile project(path: ':core', configuration: 'testArtifacts') + testCompile project(':cryptography-bc') +} diff --git a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt new file mode 100644 index 0000000..5a5d6bc --- /dev/null +++ b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt @@ -0,0 +1,81 @@ +/* + * 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.exports + +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.Encode +import com.beust.klaxon.* +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.util.* + +/** + * Exports and imports contacts and identities + */ +object ContactExport { + fun exportContacts(contacts: List<BitmessageAddress>, includePrivateKey: Boolean = false) = json { + val base64 = Base64.getEncoder() + array( + contacts.map { + obj( + "alias" to it.alias, + "address" to it.address, + "chan" to it.isChan, + "subscribed" to it.isSubscribed, + "pubkey" to it.pubkey?.let { + val out = ByteArrayOutputStream() + it.writeUnencrypted(out) + base64.encodeToString(out.toByteArray()) + }, + "privateKey" to if (includePrivateKey) { + it.privateKey?.let { base64.encodeToString(Encode.bytes(it)) } + } else { + null + } + ) + } + ) + } + + fun importContacts(input: JsonArray<*>): List<BitmessageAddress> { + return input.filterIsInstance(JsonObject::class.java).map { json -> + val base64 = Base64.getDecoder() + fun JsonObject.bytes(fieldName: String) = string(fieldName)?.let { base64.decode(it) } + val privateKey = json.bytes("privateKey")?.let { PrivateKey.read(ByteArrayInputStream(it)) } + if (privateKey != null) { + BitmessageAddress(privateKey) + } else { + BitmessageAddress(json.string("address") ?: throw IllegalArgumentException("address expected")) + }.apply { + alias = json.string("alias") + isChan = json.boolean("chan") ?: false + isSubscribed = json.boolean("subscribed") ?: false + pubkey = json.bytes("pubkey")?.let { + Factory.readPubkey( + version = version, + stream = stream, + `is` = ByteArrayInputStream(it), + length = it.size, + encrypted = false + ) + } + } + } + } +} diff --git a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt new file mode 100644 index 0000000..8b4bd4e --- /dev/null +++ b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt @@ -0,0 +1,106 @@ +/* + * 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.exports + +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.utils.Encode +import ch.dissem.bitmessage.utils.TTL +import com.beust.klaxon.* +import java.util.* + +/** + * Exports and imports messages and labels + */ +object MessageExport { + fun exportLabels(labels: List<Label>) = json { + array( + labels.map { + obj( + "label" to it.toString(), + "type" to it.type?.name, + "color" to it.color + ) + } + ) + } + + fun exportMessages(messages: List<Plaintext>) = json { + val base64 = Base64.getEncoder() + array(messages.map { + obj( + "type" to it.type.name, + "from" to it.from.address, + "to" to it.to?.address, + "subject" to it.subject, + "body" to it.text, + + "conversationId" to it.conversationId.toString(), + "msgId" to it.inventoryVector?.hash?.let { base64.encodeToString(it) }, + "encoding" to it.encodingCode, + "status" to it.status.name, + "message" to base64.encodeToString(it.message), + "ackData" to it.ackData?.let { base64.encodeToString(it) }, + "ackMessage" to it.ackMessage?.let { base64.encodeToString(Encode.bytes(it)) }, + "signature" to it.signature?.let { base64.encodeToString(it) }, + "sent" to it.sent, + "received" to it.received, + "ttl" to it.ttl, + "labels" to array(it.labels.map { it.toString() }) + ) + }) + } + + fun importMessages(input: JsonArray<*>, labels: Map<String, Label>): List<Plaintext> { + return input.filterIsInstance(JsonObject::class.java).map { json -> + val base64 = Base64.getDecoder() + fun JsonObject.bytes(fieldName: String) = string(fieldName)?.let { base64.decode(it) } + Plaintext.Builder(Plaintext.Type.valueOf(json.string("type") ?: "MSG")) + .from(json.string("from")?.let { BitmessageAddress(it) } ?: throw IllegalArgumentException("'from' address expected")) + .to(json.string("to")?.let { BitmessageAddress(it) }) + .conversation(json.string("conversationId")?.let { UUID.fromString(it) } ?: UUID.randomUUID()) + .IV(json.bytes("msgId")?.let { InventoryVector(it) }) + .encoding(json.long("encoding") ?: throw IllegalArgumentException("encoding expected")) + .status(json.string("status")?.let { Plaintext.Status.valueOf(it) } ?: throw IllegalArgumentException("status expected")) + .message(json.bytes("message") ?: throw IllegalArgumentException("message expected")) + .ackData(json.bytes("ackData")) + .ackMessage(json.bytes("ackMessage")) + .signature(json.bytes("signature")) + .sent(json.long("sent")) + .received(json.long("received")) + .ttl(json.long("ttl") ?: TTL.msg) + .labels( + json.array<String>("labels")?.map { labels[it] }?.filterNotNull() ?: emptyList() + ) + .build() + } + } + + fun importLabels(input: JsonArray<Any?>): List<Label> { + return input.filterIsInstance(JsonObject::class.java).map { json -> + Label( + label = json.string("label") ?: throw IllegalArgumentException("label expected"), + type = json.string("type")?.let { Label.Type.valueOf(it) }, + color = json.int("color") ?: 0 + ) + } + } + + fun createLabelMap(labels: List<Label>) = labels.associateBy { it.toString() } +} diff --git a/exports/src/test/kotlin/ch/dissem/bitmessage/exports/ContactExportTest.kt b/exports/src/test/kotlin/ch/dissem/bitmessage/exports/ContactExportTest.kt new file mode 100644 index 0000000..dd3aa5f --- /dev/null +++ b/exports/src/test/kotlin/ch/dissem/bitmessage/exports/ContactExportTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.exports + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.utils.TestUtils +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.nullValue +import org.junit.Assert.assertThat +import org.junit.Test + +class ContactExportTest { + + init { + TestUtils.mockedInternalContext(cryptography = BouncyCryptography()) + } + + @Test + fun `ensure contacts are exported`() { + val alice = BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj") + alice.alias = "Alice" + alice.isSubscribed = true + val contacts = listOf( + BitmessageAddress("BM-2cWJ4UFRTCehWuWNsW8fJkAYMxU4S8jxci"), + TestUtils.loadContact(), + alice + ) + val export = ContactExport.exportContacts(contacts) + print(export.toJsonString(true)) + assertThat(ContactExport.importContacts(export), `is`(contacts)) + } + + @Test + fun `ensure private keys are omitted by default`() { + val contacts = listOf( + BitmessageAddress.chan(1, "test") + ) + val export = ContactExport.exportContacts(contacts) + print(export.toJsonString(true)) + val import = ContactExport.importContacts(export) + assertThat(import.size, `is`(1)) + assertThat(import[0].isChan, `is`(true)) + assertThat(import[0].privateKey, `is`(nullValue())) + } + + @Test + fun `ensure private keys are exported if flag is set`() { + val contacts = listOf( + BitmessageAddress.chan(1, "test") + ) + val export = ContactExport.exportContacts(contacts, true) + print(export.toJsonString(true)) + val import = ContactExport.importContacts(export) + assertThat(import.size, `is`(1)) + assertThat(import[0].isChan, `is`(true)) + assertThat(import[0].privateKey, `is`(contacts[0].privateKey)) + } +} diff --git a/exports/src/test/kotlin/ch/dissem/bitmessage/exports/MessageExportTest.kt b/exports/src/test/kotlin/ch/dissem/bitmessage/exports/MessageExportTest.kt new file mode 100644 index 0000000..04fd799 --- /dev/null +++ b/exports/src/test/kotlin/ch/dissem/bitmessage/exports/MessageExportTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.exports + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.utils.ConversationServiceTest +import ch.dissem.bitmessage.utils.Singleton +import ch.dissem.bitmessage.utils.TestUtils +import org.hamcrest.CoreMatchers.`is` +import org.junit.Assert.assertThat +import org.junit.Test + +class MessageExportTest { + val inbox = Label("Inbox", Label.Type.INBOX, 0x0000ff) + val outbox = Label("Outbox", Label.Type.OUTBOX, 0x00ff00) + val unread = Label("Unread", Label.Type.UNREAD, 0x000000) + val trash = Label("Trash", Label.Type.TRASH, 0x555555) + + val labels = listOf( + inbox, + outbox, + unread, + trash + ) + val labelMap = MessageExport.createLabelMap(labels) + + init { + TestUtils.mockedInternalContext(cryptography = BouncyCryptography()) + } + + @Test + fun `ensure labels are exported`() { + val export = MessageExport.exportLabels(labels) + print(export.toJsonString(true)) + assertThat(MessageExport.importLabels(export), `is`(labels)) + } + + @Test + fun `ensure messages are exported`() { + val messages = listOf( + Plaintext.Builder(Plaintext.Type.MSG) + .ackData(Singleton.cryptography().randomBytes(32)) + .from(BitmessageAddress("BM-2cWJ4UFRTCehWuWNsW8fJkAYMxU4S8jxci")) + .to(BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn")) + .message("Subject", "Message") + .status(Plaintext.Status.RECEIVED) + .labels(listOf(inbox)) + .build(), + Plaintext.Builder(Plaintext.Type.BROADCAST) + .from(BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .message("Subject", "Message") + .labels(listOf(inbox, unread)) + .status(Plaintext.Status.SENT) + .build(), + Plaintext.Builder(Plaintext.Type.MSG) + .ttl(1) + .message("subject", "message") + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .labels(listOf(trash)) + .status(Plaintext.Status.SENT_ACKNOWLEDGED) + .build(), + *ConversationServiceTest.conversation( + TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), + TestUtils.loadContact() + ).toTypedArray() + ) + val export = MessageExport.exportMessages(messages) + print(export.toJsonString(true)) + assertThat(MessageExport.importMessages(export, labelMap), `is`(messages)) + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0cf2d0f..65d177a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Jul 02 11:22:52 CEST 2017 +#Mon Jul 17 06:32:41 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip diff --git a/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt index 990b31d..a94cd1f 100644 --- a/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt +++ b/networking/src/test/kotlin/ch/dissem/bitmessage/networking/NetworkHandlerTest.kt @@ -241,7 +241,6 @@ class NetworkHandlerTest { Thread.sleep(100) } catch (ignore: InterruptedException) { } - } while (ctx.isRunning()) } diff --git a/settings.gradle b/settings.gradle index 4caa813..59771e2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,4 +14,6 @@ include 'cryptography-sc' include 'cryptography-bc' -include 'extensions' \ No newline at end of file +include 'extensions' + +include 'exports' From d9e52c85c36c2cb4f32cfd456b9ac8d9a11cdade Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 22 Jul 2017 08:01:59 +0200 Subject: [PATCH 3/7] Fixed occasional on the selector - it shouldn't crash the application --- .../networking/nio/NioNetworkHandler.kt | 126 +++++++++--------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt index b926d7d..2bf4c18 100644 --- a/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt +++ b/networking/src/main/kotlin/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.kt @@ -203,73 +203,77 @@ class NioNetworkHandler : NetworkHandler, InternalContext.ContextHolder { serverChannel.register(selector, OP_ACCEPT, null) while (selector.isOpen) { - selector.select(1000) - val keyIterator = selector.selectedKeys().iterator() - while (keyIterator.hasNext()) { - val key = keyIterator.next() - keyIterator.remove() - if (key.attachment() == null) { - try { - if (key.isAcceptable) { - // handle accept - try { - val accepted = (key.channel() as ServerSocketChannel).accept() - accepted.configureBlocking(false) - val connection = Connection(ctx, SERVER, - NetworkAddress( - time = now, - stream = 1L, - socket = accepted.socket()!! - ), - requestedObjects, 0 - ) - connections.put( - connection, - accepted.register(selector, OP_READ or OP_WRITE, connection) - ) - } catch (e: AsynchronousCloseException) { - LOG.trace(e.message) - } catch (e: IOException) { - LOG.error(e.message, e) - } + try { + selector.select(1000) + val keyIterator = selector.selectedKeys().iterator() + while (keyIterator.hasNext()) { + val key = keyIterator.next() + keyIterator.remove() + if (key.attachment() == null) { + try { + if (key.isAcceptable) { + // handle accept + try { + val accepted = (key.channel() as ServerSocketChannel).accept() + accepted.configureBlocking(false) + val connection = Connection(ctx, SERVER, + NetworkAddress( + time = now, + stream = 1L, + socket = accepted.socket()!! + ), + requestedObjects, 0 + ) + connections.put( + connection, + accepted.register(selector, OP_READ or OP_WRITE, connection) + ) + } catch (e: AsynchronousCloseException) { + LOG.trace(e.message) + } catch (e: IOException) { + LOG.error(e.message, e) + } - } - } catch (e: CancelledKeyException) { - LOG.debug(e.message, e) - } - - } else { - // handle read/write - val channel = key.channel() as SocketChannel - val connection = key.attachment() as Connection - try { - if (key.isConnectable) { - if (!channel.finishConnect()) { - continue } + } catch (e: CancelledKeyException) { + LOG.debug(e.message, e) } - if (key.isWritable) { - write(channel, connection.io) + + } else { + // handle read/write + val channel = key.channel() as SocketChannel + val connection = key.attachment() as Connection + try { + if (key.isConnectable) { + if (!channel.finishConnect()) { + continue + } + } + if (key.isWritable) { + write(channel, connection.io) + } + if (key.isReadable) { + read(channel, connection.io) + } + if (connection.state == Connection.State.DISCONNECTED) { + key.interestOps(0) + channel.close() + } else if (connection.io.isWritePending) { + key.interestOps(OP_READ or OP_WRITE) + } else { + key.interestOps(OP_READ) + } + } catch (e: CancelledKeyException) { + connection.disconnect() + } catch (e: NodeException) { + connection.disconnect() + } catch (e: IOException) { + connection.disconnect() } - if (key.isReadable) { - read(channel, connection.io) - } - if (connection.state == Connection.State.DISCONNECTED) { - key.interestOps(0) - channel.close() - } else if (connection.io.isWritePending) { - key.interestOps(OP_READ or OP_WRITE) - } else { - key.interestOps(OP_READ) - } - } catch (e: CancelledKeyException) { - connection.disconnect() - } catch (e: NodeException) { - connection.disconnect() - } catch (e: IOException) { - connection.disconnect() } } + } catch (e: CancelledKeyException) { + LOG.debug(e.message, e) } // set interest ops for ((connection, selectionKey) in connections) { From 6e79b0c50fe1d24d71bc73be118d0ef2ffcb1d61 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 29 Jul 2017 14:56:03 +0200 Subject: [PATCH 4/7] Minor improvements and fixes I copied the Base64 encoder from Android platform (and converted it to Kotlin) because the Java one exists only since 1.8 (I don't now if I want to curse Java for not supporting Base64 out of the box earlier, or Android for not supporting a recent Java API) --- build.gradle | 2 +- .../ports/AbstractMessageRepository.kt | 2 + .../bitmessage/ports/MessageRepository.kt | 2 + .../ch/dissem/bitmessage/utils/Base58.kt | 13 +- .../ch/dissem/bitmessage/utils/Base64.kt | 664 ++++++++++++++++++ .../ch/dissem/bitmessage/utils/UnixTime.kt | 6 +- core/src/main/resources/nodes.txt | 2 +- .../ch/dissem/bitmessage/utils/Base64Test.kt | 35 + .../bitmessage/exports/ContactExport.kt | 10 +- .../bitmessage/exports/MessageExport.kt | 15 +- 10 files changed, 726 insertions(+), 25 deletions(-) create mode 100644 core/src/main/kotlin/ch/dissem/bitmessage/utils/Base64.kt create mode 100644 core/src/test/kotlin/ch/dissem/bitmessage/utils/Base64Test.kt diff --git a/build.gradle b/build.gradle index 41f5805..669ad8e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.1.3' + ext.kotlin_version = '1.1.3-2' repositories { mavenCentral() } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt index b6d2108..d426dac 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractMessageRepository.kt @@ -49,6 +49,8 @@ abstract class AbstractMessageRepository : MessageRepository, InternalContext.Co } } + override fun getAllMessages() = find("1=1") + override fun getMessage(id: Any): Plaintext { if (id is Long) { return single(find("id=" + id)) ?: throw IllegalArgumentException("There is no message with id $id") diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt index 51e0d02..9279dc0 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt @@ -30,6 +30,8 @@ interface MessageRepository { fun countUnread(label: Label?): Int + fun getAllMessages(): List<Plaintext> + fun getMessage(id: Any): Plaintext fun getMessage(iv: InventoryVector): Plaintext? diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt index 7f98f93..216e07d 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base58.kt @@ -88,21 +88,22 @@ object Base58 { val input58 = ByteArray(input.length) // Transform the String to a base58 byte sequence for (i in 0..input.length - 1) { - val c = input[i] + val c = input[i].toInt() - var digit58 = -1 - if (c.toInt() < 128) { - digit58 = INDEXES[c.toInt()] + val digit58 = if (c < 128) { + INDEXES[c] + } else { + -1 } if (digit58 < 0) { - throw AddressFormatException("Illegal character $c at $i") + throw AddressFormatException("Illegal character ${input[i]} at $i") } input58[i] = digit58.toByte() } // Count leading zeroes var zeroCount = 0 - while (zeroCount < input58.size && input58[zeroCount].toInt() == 0) { + while (zeroCount < input58.size && input58[zeroCount] == 0.toByte()) { ++zeroCount } // The encoding diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base64.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base64.kt new file mode 100644 index 0000000..f37415e --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/Base64.kt @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 + + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs <a + * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a + * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>. + */ +object Base64 { + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + const val NO_PADDING = 1 + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + const val NO_WRAP = 2 + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if `NO_WRAP` is specified as well. + */ + const val CRLF = 4 + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * `-` and `_` are used in place of `+` and + * `/`. + */ + const val URL_SAFE = 8 + + /** + * Default values for encoder/decoder flags. + */ + const val DEFAULT = NO_WRAP + + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + + * + * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + + * @param str the input String to decode, which is converted to + * * bytes using the default charset + * * + * @param flags controls certain features of the decoded output. + * * Pass `DEFAULT` to decode standard Base64. + * * + * * + * @throws IllegalArgumentException if the input contains + * * incorrect padding + */ + @JvmStatic + fun decode(str: String, flags: Int = DEFAULT): ByteArray { + val input = str.toByteArray(Charsets.US_ASCII) + return decode(input, 0, input.size, flags) + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + + * + * The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + + * @param input the data to decode + * * + * @param offset the position within the input array at which to start + * * + * @param len the number of bytes of input to decode + * * + * @param flags controls certain features of the decoded output. + * * Pass `DEFAULT` to decode standard Base64. + * * + * * + * @throws IllegalArgumentException if the input contains + * * incorrect padding + */ + @JvmStatic + fun decode(input: ByteArray, offset: Int, len: Int, flags: Int = DEFAULT): ByteArray { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + val decoder = Decoder(Options(flags), ByteArray(len * 3 / 4)) + + if (!decoder.process(input, offset, len, true)) { + throw IllegalArgumentException("bad base-64") + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.size) { + return decoder.output + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + val temp = ByteArray(decoder.op) + System.arraycopy(decoder.output, 0, temp, 0, decoder.op) + return temp + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + + * @param input the data to encode + * * + * @param flags controls certain features of the encoded output. + * * Passing `DEFAULT` results in output that + * * adheres to RFC 2045. + */ + fun encodeToString(input: ByteArray, flags: Int = DEFAULT): String { + return String(encode(input, 0, input.size, flags), Charsets.US_ASCII) + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + + * @param input the data to encode + * * + * @param offset the position within the input array at which to + * * start + * * + * @param len the number of bytes of input to encode + * * + * @param flags controls certain features of the encoded output. + * * Passing `DEFAULT` results in output that + * * adheres to RFC 2045. + */ + fun encode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray { + // Compute the exact length of the array we will produce. + var output_len = len / 3 * 4 + + val options = Options(flags) + + // Account for the tail of the data and the padding bytes, if any. + if (options.do_padding) { + if (len % 3 > 0) { + output_len += 4 + } + } else { + when (len % 3) { + 0 -> { + } + 1 -> output_len += 2 + 2 -> output_len += 3 + } + } + + // Account for the newlines, if any. + if (options.do_newline && len > 0) { + output_len += ((len - 1) / (3 * Encoder.LINE_GROUPS) + 1) * if (options.do_cr) 2 else 1 + } + + val encoder = Encoder(options, ByteArray(output_len)) + encoder.process(input, offset, len, true) + + assert(encoder.op == output_len) + + return encoder.output + } +} + +class Options(flags: Int) { + val do_padding = flags and Base64.NO_PADDING == 0 + val do_newline = flags and Base64.NO_WRAP == 0 + val do_cr = flags and Base64.CRLF != 0 + val url_safe = flags and Base64.URL_SAFE == 0 +} + +private abstract class Coder(val output: ByteArray) { + var op = 0 + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + abstract fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + abstract fun maxOutputSize(len: Int): Int +} + +private class Decoder(options: Options, output: ByteArray) : Coder(output) { + + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private var state: Int = 0 // state number (0 to 6) + private var value: Int = 0 + + private val alphabet = if (options.url_safe) DECODE else DECODE_WEBSAFE + + /** + * @return an overestimate for the number of bytes `len` bytes could decode to. + */ + override fun maxOutputSize(len: Int): Int { + return len * 3 / 4 + 10 + } + + /** + * Decode another block of input data. + + * @return true if the state machine is still healthy. false if + * * bad base-64 data has been detected in the input stream. + */ + override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean { + var end = len + if (this.state == 6) return false + + var p = offset + end += offset + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + var state = this.state + var value = this.value + var op = 0 + val output = this.output + val alphabet = this.alphabet + + while (p < end) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + fun nextVal(): Int { + value = alphabet[input[p].toInt() and 0xff] shl 18 or + (alphabet[input[p + 1].toInt() and 0xff] shl 12) or + (alphabet[input[p + 2].toInt() and 0xff] shl 6) or + alphabet[input[p + 3].toInt() and 0xff] + return value + } + while (p + 4 <= end && nextVal() >= 0) { + output[op + 2] = value.toByte() + output[op + 1] = (value shr 8).toByte() + output[op] = (value shr 16).toByte() + op += 3 + p += 4 + } + if (p >= end) break + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + val d = alphabet[input[p++].toInt() and 0xff] + + when (state) { + 0 -> if (d >= 0) { + value = d + ++state + } else if (d != SKIP) { + this.state = 6 + return false + } + + 1 -> if (d >= 0) { + value = value shl 6 or d + ++state + } else if (d != SKIP) { + this.state = 6 + return false + } + + 2 -> if (d >= 0) { + value = value shl 6 or d + ++state + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (value shr 4).toByte() + state = 4 + } else if (d != SKIP) { + this.state = 6 + return false + } + + 3 -> if (d >= 0) { + // Emit the output triple and return to state 0. + value = value shl 6 or d + output[op + 2] = value.toByte() + output[op + 1] = (value shr 8).toByte() + output[op] = (value shr 16).toByte() + op += 3 + state = 0 + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op + 1] = (value shr 2).toByte() + output[op] = (value shr 10).toByte() + op += 2 + state = 5 + } else if (d != SKIP) { + this.state = 6 + return false + } + + 4 -> if (d == EQUALS) { + ++state + } else if (d != SKIP) { + this.state = 6 + return false + } + + 5 -> if (d != SKIP) { + this.state = 6 + return false + } + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state + this.value = value + this.op = op + return true + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + when (state) { + 0 -> { + } + 1 -> { + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6 + return false + } + 2 -> + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (value shr 4).toByte() + 3 -> { + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (value shr 10).toByte() + output[op++] = (value shr 2).toByte() + } + 4 -> { + // Read one padding '=' when we expected 2. Illegal. + this.state = 6 + return false + } + 5 -> { + } + }// Output length is a multiple of three. Fine. + // Read all the padding '='s we expected and no more. + // Fine. + + this.state = state + this.op = op + return true + } + + companion object { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private val DECODE = intArrayOf( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ) + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private val DECODE_WEBSAFE = intArrayOf( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ) + + /** Non-data values in the DECODE arrays. */ + private val SKIP = -1 + private val EQUALS = -2 + } +} + +private class Encoder(val options: Options, output: ByteArray) : Coder(output) { + + private val alphabet: ByteArray = if (options.url_safe) ENCODE else ENCODE_WEBSAFE + + private val tail = ByteArray(2) + private var tailLen = 0 + private var count = if (options.do_newline) LINE_GROUPS else -1 + + /** + * @return an overestimate for the number of bytes `len` bytes could encode to. + */ + override fun maxOutputSize(len: Int): Int { + return len * 8 / 5 + 10 + } + + override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean { + var end = len + // Using local variables makes the encoder about 9% faster. + val alphabet = this.alphabet + val output = this.output + var op = 0 + var count = this.count + + var p = offset + end += offset + var v = -1 + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + + when (tailLen) { + 0 -> { + } + + 1 -> { + if (p + 2 <= end) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = tail[0].toInt() and 0xff shl 16 or + (input[p++].toInt() and 0xff shl 8) or + (input[p++].toInt() and 0xff) + tailLen = 0 + } + } + + 2 -> if (p + 1 <= end) { + // A 2-byte tail with at least 1 byte of input. + v = tail[0].toInt() and 0xff shl 16 or + (tail[1].toInt() and 0xff shl 8) or + (input[p++].toInt() and 0xff) + tailLen = 0 + } + }// There was no tail. + + if (v != -1) { + output[op++] = alphabet[v shr 18 and 0x3f] + output[op++] = alphabet[v shr 12 and 0x3f] + output[op++] = alphabet[v shr 6 and 0x3f] + output[op++] = alphabet[v and 0x3f] + if (--count == 0) { + if (options.do_cr) output[op++] = CR + output[op++] = NL + count = LINE_GROUPS + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p + 3 <= end) { + v = input[p].toInt() and 0xff shl 16 or + (input[p + 1].toInt() and 0xff shl 8) or + (input[p + 2].toInt() and 0xff) + output[op] = alphabet[v shr 18 and 0x3f] + output[op + 1] = alphabet[v shr 12 and 0x3f] + output[op + 2] = alphabet[v shr 6 and 0x3f] + output[op + 3] = alphabet[v and 0x3f] + p += 3 + op += 4 + if (--count == 0) { + if (options.do_cr) output[op++] = CR + output[op++] = NL + count = LINE_GROUPS + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p - tailLen == end - 1) { + var t = 0 + v = (if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff shl 4 + tailLen -= t + output[op++] = alphabet[v shr 6 and 0x3f] + output[op++] = alphabet[v and 0x3f] + if (options.do_padding) { + output[op++] = PAD + output[op++] = PAD + } + if (options.do_newline) { + if (options.do_cr) output[op++] = CR + output[op++] = NL + } + } else if (p - tailLen == end - 2) { + var t = 0 + v = (if (tailLen > 1) tail[t++] else input[p++]).toInt() and 0xff shl 10 or ((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff shl 2) + tailLen -= t + output[op++] = alphabet[v shr 12 and 0x3f] + output[op++] = alphabet[v shr 6 and 0x3f] + output[op++] = alphabet[v and 0x3f] + if (options.do_padding) { + output[op++] = PAD + } + if (options.do_newline) { + if (options.do_cr) output[op++] = CR + output[op++] = NL + } + } else if (options.do_newline && op > 0 && count != LINE_GROUPS) { + if (options.do_cr) output[op++] = CR + output[op++] = NL + } + + assert(tailLen == 0) + assert(p == end) + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == end - 1) { + tail[tailLen++] = input[p] + } else if (p == end - 2) { + tail[tailLen++] = input[p] + tail[tailLen++] = input[p + 1] + } + } + + this.op = op + this.count = count + + return true + } + + companion object { + private const val PAD = '='.toByte() + private const val CR = '\r'.toByte() + private const val NL = '\n'.toByte() + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * [RFC 2045](http://www.ietf.org/rfc/rfc2045.txt)). + */ + val LINE_GROUPS = 19 + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private val ENCODE = charArrayOf( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + ).map { it.toByte() }.toByteArray() + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private val ENCODE_WEBSAFE = charArrayOf( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + ).map { it.toByte() }.toByteArray() + } +} diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt b/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt index d21bc2e..b542bd9 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/utils/UnixTime.kt @@ -23,15 +23,15 @@ object UnixTime { /** * Length of a minute in seconds, intended for use with [.now]. */ - @JvmField val MINUTE = 60L + const val MINUTE = 60L /** * Length of an hour in seconds, intended for use with [.now]. */ - @JvmField val HOUR = 60L * MINUTE + const val HOUR = 60L * MINUTE /** * Length of a day in seconds, intended for use with [.now]. */ - @JvmField val DAY = 24L * HOUR + const val DAY = 24L * HOUR /** * @return the time in second based Unix time ([System.currentTimeMillis]/1000) diff --git a/core/src/main/resources/nodes.txt b/core/src/main/resources/nodes.txt index bce982e..23e85d2 100644 --- a/core/src/main/resources/nodes.txt +++ b/core/src/main/resources/nodes.txt @@ -1,6 +1,6 @@ [stream 1] -dissem.ch:8444 +bitmessage.dissem.ch:8444 bootstrap8080.bitmessage.org:8080 bootstrap8444.bitmessage.org:8444 diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/utils/Base64Test.kt b/core/src/test/kotlin/ch/dissem/bitmessage/utils/Base64Test.kt new file mode 100644 index 0000000..35f9e3a --- /dev/null +++ b/core/src/test/kotlin/ch/dissem/bitmessage/utils/Base64Test.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography +import org.hamcrest.Matchers.`is` +import org.junit.Assert.assertThat +import org.junit.Test + +class Base64Test { + @Test + fun `ensure data is encoded and decoded correctly`() { + val cryptography = BouncyCryptography() + for (i in 100..200) { + val data = cryptography.randomBytes(i) + val string = Base64.encodeToString(data) + val decoded = Base64.decode(string) + assertThat(decoded, `is`(data)) + } + } +} diff --git a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt index 5a5d6bc..e429881 100644 --- a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt +++ b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/ContactExport.kt @@ -19,18 +19,17 @@ package ch.dissem.bitmessage.exports import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.valueobject.PrivateKey import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.utils.Base64 import ch.dissem.bitmessage.utils.Encode import com.beust.klaxon.* import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream -import java.util.* /** * Exports and imports contacts and identities */ object ContactExport { fun exportContacts(contacts: List<BitmessageAddress>, includePrivateKey: Boolean = false) = json { - val base64 = Base64.getEncoder() array( contacts.map { obj( @@ -41,10 +40,10 @@ object ContactExport { "pubkey" to it.pubkey?.let { val out = ByteArrayOutputStream() it.writeUnencrypted(out) - base64.encodeToString(out.toByteArray()) + Base64.encodeToString(out.toByteArray()) }, "privateKey" to if (includePrivateKey) { - it.privateKey?.let { base64.encodeToString(Encode.bytes(it)) } + it.privateKey?.let { Base64.encodeToString(Encode.bytes(it)) } } else { null } @@ -55,8 +54,7 @@ object ContactExport { fun importContacts(input: JsonArray<*>): List<BitmessageAddress> { return input.filterIsInstance(JsonObject::class.java).map { json -> - val base64 = Base64.getDecoder() - fun JsonObject.bytes(fieldName: String) = string(fieldName)?.let { base64.decode(it) } + fun JsonObject.bytes(fieldName: String) = string(fieldName)?.let { Base64.decode(it) } val privateKey = json.bytes("privateKey")?.let { PrivateKey.read(ByteArrayInputStream(it)) } if (privateKey != null) { BitmessageAddress(privateKey) diff --git a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt index 8b4bd4e..2b195cd 100644 --- a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt +++ b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt @@ -20,6 +20,7 @@ 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.utils.Base64 import ch.dissem.bitmessage.utils.Encode import ch.dissem.bitmessage.utils.TTL import com.beust.klaxon.* @@ -42,7 +43,6 @@ object MessageExport { } fun exportMessages(messages: List<Plaintext>) = json { - val base64 = Base64.getEncoder() array(messages.map { obj( "type" to it.type.name, @@ -52,13 +52,13 @@ object MessageExport { "body" to it.text, "conversationId" to it.conversationId.toString(), - "msgId" to it.inventoryVector?.hash?.let { base64.encodeToString(it) }, + "msgId" to it.inventoryVector?.hash?.let { Base64.encodeToString(it) }, "encoding" to it.encodingCode, "status" to it.status.name, - "message" to base64.encodeToString(it.message), - "ackData" to it.ackData?.let { base64.encodeToString(it) }, - "ackMessage" to it.ackMessage?.let { base64.encodeToString(Encode.bytes(it)) }, - "signature" to it.signature?.let { base64.encodeToString(it) }, + "message" to Base64.encodeToString(it.message), + "ackData" to it.ackData?.let { Base64.encodeToString(it) }, + "ackMessage" to it.ackMessage?.let { Base64.encodeToString(Encode.bytes(it)) }, + "signature" to it.signature?.let { Base64.encodeToString(it) }, "sent" to it.sent, "received" to it.received, "ttl" to it.ttl, @@ -69,8 +69,7 @@ object MessageExport { fun importMessages(input: JsonArray<*>, labels: Map<String, Label>): List<Plaintext> { return input.filterIsInstance(JsonObject::class.java).map { json -> - val base64 = Base64.getDecoder() - fun JsonObject.bytes(fieldName: String) = string(fieldName)?.let { base64.decode(it) } + fun JsonObject.bytes(fieldName: String) = string(fieldName)?.let { Base64.decode(it) } Plaintext.Builder(Plaintext.Type.valueOf(json.string("type") ?: "MSG")) .from(json.string("from")?.let { BitmessageAddress(it) } ?: throw IllegalArgumentException("'from' address expected")) .to(json.string("to")?.let { BitmessageAddress(it) }) From c81c89197b0491ec09e30bd22f78978dcc3295e0 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 11 Aug 2017 12:32:20 +0200 Subject: [PATCH 5/7] Very minor code improvements --- .../bitmessage/ports/AbstractCryptography.kt | 6 ++--- .../bitmessage/security/CryptographyTest.kt | 24 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt index f6b779b..5792f0f 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AbstractCryptography.kt @@ -198,10 +198,10 @@ abstract class AbstractCryptography protected constructor(@JvmField protected va } companion object { - protected val LOG = LoggerFactory.getLogger(Cryptography::class.java) + 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) + private val TWO_POW_64 = TWO.pow(64)!! + private val TWO_POW_16 = TWO.pow(16)!! } } diff --git a/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt b/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt index 1897512..dbd4980 100644 --- a/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt +++ b/cryptography-bc/src/test/kotlin/ch/dissem/bitmessage/security/CryptographyTest.kt @@ -23,10 +23,7 @@ import ch.dissem.bitmessage.entity.valueobject.PrivateKey import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine import ch.dissem.bitmessage.ports.ProofOfWorkEngine -import ch.dissem.bitmessage.utils.CallbackWaiter -import ch.dissem.bitmessage.utils.Singleton -import ch.dissem.bitmessage.utils.TestUtils -import ch.dissem.bitmessage.utils.UnixTime +import ch.dissem.bitmessage.utils.* import ch.dissem.bitmessage.utils.UnixTime.DAY import ch.dissem.bitmessage.utils.UnixTime.MINUTE import ch.dissem.bitmessage.utils.UnixTime.now @@ -34,7 +31,6 @@ import org.hamcrest.CoreMatchers.`is` import org.junit.Assert.* import org.junit.Test import java.io.ByteArrayInputStream -import java.io.IOException import javax.xml.bind.DatatypeConverter /** @@ -63,12 +59,12 @@ class CryptographyTest { } @Test - fun ensureDoubleHashYieldsSameResultAsHashOfHash() { + fun `ensure double hash yields same result as hash of hash`() { assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)) } - @Test(expected = IOException::class) - fun ensureExceptionForInsufficientProofOfWork() { + @Test(expected = InsufficientProofOfWorkException::class) + fun `ensure exception for insufficient proof of work`() { val objectMessage = ObjectMessage.Builder() .nonce(ByteArray(8)) .expiresTime(UnixTime.now + 28 * DAY) @@ -79,7 +75,7 @@ class CryptographyTest { } @Test - fun testDoProofOfWork() { + fun `ensure proof of work is calculated correctly`() { TestUtils.mockedInternalContext( cryptography = crypto, proofOfWorkEngine = MultiThreadedPOWEngine() @@ -108,7 +104,7 @@ class CryptographyTest { } @Test - fun ensureEncryptionAndDecryptionWorks() { + fun `ensure encryption and decryption works`() { val data = crypto.randomBytes(100) val key_e = crypto.randomBytes(32) val iv = crypto.randomBytes(16) @@ -118,7 +114,7 @@ class CryptographyTest { } @Test(expected = IllegalArgumentException::class) - fun ensureDecryptionFailsWithInvalidCypherText() { + fun `ensure decryption fails with invalid cypher text`() { val data = crypto.randomBytes(128) val key_e = crypto.randomBytes(32) val iv = crypto.randomBytes(16) @@ -126,7 +122,7 @@ class CryptographyTest { } @Test - fun testMultiplication() { + fun `ensure multiplication works correctly`() { val a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE) val A = crypto.createPublicKey(a) @@ -137,7 +133,7 @@ class CryptographyTest { } @Test - fun ensureSignatureIsValid() { + fun `ensure signature is valid`() { val data = crypto.randomBytes(100) val privateKey = PrivateKey(false, 1, 1000, 1000) val signature = crypto.getSignature(data, privateKey) @@ -145,7 +141,7 @@ class CryptographyTest { } @Test - fun ensureSignatureIsInvalidForTemperedData() { + fun `ensure signature is invalid for tempered data`() { val data = crypto.randomBytes(100) val privateKey = PrivateKey(false, 1, 1000, 1000) val signature = crypto.getSignature(data, privateKey) From cf6b3e2603e00dadea2c405f8930f2a0c3321cae Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 11 Aug 2017 17:35:46 +0200 Subject: [PATCH 6/7] Added easy way to disable acknowledges and fixed possible issue with builder-constructor --- .../ch/dissem/bitmessage/entity/Plaintext.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt index 72506b6..aa28f6e 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/Plaintext.kt @@ -229,7 +229,8 @@ class Plaintext private constructor( ) constructor(builder: Builder) : this( - type = builder.type, + // Calling prepare() here is somewhat ugly, but also a foolproof way to make sure the builder is properly initialized + type = builder.prepare().type, from = builder.from ?: throw IllegalStateException("sender identity not set"), to = builder.to, encodingCode = builder.encoding, @@ -532,6 +533,7 @@ class Plaintext private constructor( private var nonceTrialsPerByte: Long = 0 private var extraBytes: Long = 0 private var destinationRipe: ByteArray? = null + private var preventAck: Boolean = false internal var encoding: Long = 0 internal var message = ByteArray(0) internal var ackData: ByteArray? = null @@ -611,6 +613,12 @@ class Plaintext private constructor( return this } + @JvmOverloads + fun preventAck(preventAck: Boolean = true): Builder { + this.preventAck = preventAck + return this + } + fun encoding(encoding: Encoding): Builder { this.encoding = encoding.code return this @@ -700,7 +708,7 @@ class Plaintext private constructor( return this } - fun build(): Plaintext { + internal fun prepare(): Builder { if (from == null) { from = BitmessageAddress(Factory.createPubkey( addressVersion, @@ -715,12 +723,19 @@ class Plaintext private constructor( if (to == null && type != Type.BROADCAST && destinationRipe != null) { to = BitmessageAddress(0, 0, destinationRipe!!) } - if (type == MSG && ackMessage == null && ackData == null) { + if (preventAck) { + ackData = null + ackMessage = null + } else if (type == MSG && ackMessage == null && ackData == null) { ackData = cryptography().randomBytes(Msg.ACK_LENGTH) } if (ttl <= 0) { ttl = TTL.msg } + return this + } + + fun build(): Plaintext { return Plaintext(this) } } From c8dfc3b45946667ff65afe681013534a103fe0c9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu@dissem.ch> Date: Fri, 25 Aug 2017 21:11:10 +0200 Subject: [PATCH 7/7] Added option to save labels and other improvements and fixes used for exports and imports --- build.gradle | 2 +- .../bitmessage/DefaultMessageListener.kt | 22 +++--- .../bitmessage/entity/valueobject/Label.kt | 3 +- .../ports/AlreadyStoredException.kt | 23 ++++++ .../bitmessage/ports/MessageRepository.kt | 2 + .../bitmessage/BitmessageContextTest.kt | 6 +- .../bitmessage/DefaultMessageListenerTest.kt | 4 +- .../bitmessage/exports/MessageExport.kt | 2 +- .../repository/JdbcMessageRepository.kt | 79 +++++++++++++++---- 9 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 core/src/main/kotlin/ch/dissem/bitmessage/ports/AlreadyStoredException.kt diff --git a/build.gradle b/build.gradle index 669ad8e..33583e4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.1.3-2' + ext.kotlin_version = '1.1.4-2' repositories { mavenCentral() } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt b/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt index ac30587..ad32bf9 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/DefaultMessageListener.kt @@ -23,6 +23,7 @@ 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.AlreadyStoredException import ch.dissem.bitmessage.ports.Labeler import ch.dissem.bitmessage.ports.NetworkHandler import ch.dissem.bitmessage.utils.Strings.hex @@ -65,7 +66,7 @@ open class DefaultMessageListener( protected fun receive(objectMessage: ObjectMessage, getPubkey: GetPubkey) { val identity = ctx.addressRepository.findIdentity(getPubkey.ripeTag) - if (identity != null && identity.privateKey != null && !identity.isChan) { + if (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) @@ -90,7 +91,6 @@ open class DefaultMessageListener( } } catch (_: DecryptionFailedException) { } - } private fun updatePubkey(address: BitmessageAddress, pubkey: Pubkey) { @@ -157,14 +157,18 @@ open class DefaultMessageListener( msg.inventoryVector = iv labeler.setLabels(msg) - ctx.messageRepository.save(msg) - listener.receive(msg) + try { + 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") + 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") + } + } catch (e: AlreadyStoredException) { + LOG.trace("Message was already received before.", e) } } diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt index 6fd4fd6..3bba8d6 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/entity/valueobject/Label.kt @@ -25,7 +25,8 @@ data class Label( /** * RGBA representation for the color. */ - var color: Int = 0 + var color: Int = 0, + var ord: Int = 1000 ) : Serializable { var id: Any? = null diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/AlreadyStoredException.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AlreadyStoredException.kt new file mode 100644 index 0000000..619f546 --- /dev/null +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/AlreadyStoredException.kt @@ -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. + */ + +package ch.dissem.bitmessage.ports + +/** + * Should be thrown if a received and decrypted message can't be stored because it has already been received and stored. + * (So it's not announced again to the client.) + */ +class AlreadyStoredException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) diff --git a/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt index 9279dc0..68fac7a 100644 --- a/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt +++ b/core/src/main/kotlin/ch/dissem/bitmessage/ports/MessageRepository.kt @@ -28,6 +28,8 @@ interface MessageRepository { fun getLabels(vararg types: Label.Type): List<Label> + fun save(label: Label) + fun countUnread(label: Label?): Int fun getAllMessages(): List<Plaintext> diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt index 24e8ae3..d767789 100644 --- a/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt +++ b/core/src/test/kotlin/ch/dissem/bitmessage/BitmessageContextTest.kt @@ -218,7 +218,7 @@ class BitmessageContextTest { verify(ctx.internals.proofOfWorkRepository, timeout(10000)).putObject( argThat { payload.type == ObjectType.MSG }, eq(1000L), eq(1000L)) assertEquals(2, testPowRepo.added) - verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG }) + verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat<Plaintext> { type == Type.MSG }) } @Test @@ -228,7 +228,7 @@ class BitmessageContextTest { "Subject", "Message") verify(testPowRepo, timeout(10000).atLeastOnce()) .putObject(argThat { payload.type == ObjectType.GET_PUBKEY }, eq(1000L), eq(1000L)) - verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.MSG }) + verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat<Plaintext> { type == Type.MSG }) } @Test(expected = IllegalArgumentException::class) @@ -245,7 +245,7 @@ class BitmessageContextTest { verify(ctx.internals.proofOfWorkRepository, timeout(1000).atLeastOnce()) .putObject(argThat { payload.type == ObjectType.BROADCAST }, eq(1000L), eq(1000L)) verify(testPowEngine).calculateNonce(any(), any(), any()) - verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat { type == Type.BROADCAST }) + verify(ctx.messages, timeout(10000).atLeastOnce()).save(argThat<Plaintext> { type == Type.BROADCAST }) } @Test(expected = IllegalArgumentException::class) diff --git a/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt b/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt index a96430f..57db6fe 100644 --- a/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt +++ b/core/src/test/kotlin/ch/dissem/bitmessage/DefaultMessageListenerTest.kt @@ -108,7 +108,7 @@ class DefaultMessageListenerTest : TestBase() { listener.receive(objectMessage) - verify(ctx.messageRepository, atLeastOnce()).save(argThat { type == MSG }) + verify(ctx.messageRepository, atLeastOnce()).save(argThat<Plaintext> { type == MSG }) } @Test @@ -131,6 +131,6 @@ class DefaultMessageListenerTest : TestBase() { listener.receive(objectMessage) - verify(ctx.messageRepository, atLeastOnce()).save(argThat { type == BROADCAST }) + verify(ctx.messageRepository, atLeastOnce()).save(argThat<Plaintext> { type == BROADCAST }) } } diff --git a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt index 2b195cd..72cf35f 100644 --- a/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt +++ b/exports/src/main/kotlin/ch/dissem/bitmessage/exports/MessageExport.kt @@ -91,7 +91,7 @@ object MessageExport { } } - fun importLabels(input: JsonArray<Any?>): List<Label> { + fun importLabels(input: JsonArray<*>): List<Label> { return input.filterIsInstance(JsonObject::class.java).map { json -> Label( label = json.string("label") ?: throw IllegalArgumentException("label expected"), diff --git a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt index 0717e8e..7ecfbf8 100644 --- a/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt +++ b/repositories/src/main/kotlin/ch/dissem/bitmessage/repository/JdbcMessageRepository.kt @@ -20,22 +20,18 @@ import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.valueobject.InventoryVector import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.ports.AbstractMessageRepository +import ch.dissem.bitmessage.ports.AlreadyStoredException import ch.dissem.bitmessage.ports.MessageRepository import ch.dissem.bitmessage.repository.JdbcHelper.Companion.writeBlob import org.slf4j.LoggerFactory -import java.io.IOException -import java.sql.Connection -import java.sql.ResultSet -import java.sql.SQLException -import java.sql.Statement +import java.sql.* import java.util.* class JdbcMessageRepository(private val config: JdbcConfig) : AbstractMessageRepository(), MessageRepository { override fun findLabels(where: String): List<Label> { try { - config.getConnection().use { - connection -> + config.getConnection().use { connection -> return findLabels(connection, where) } } catch (e: SQLException) { @@ -51,12 +47,61 @@ class JdbcMessageRepository(private val config: JdbcConfig) : AbstractMessageRep } else { Label.Type.valueOf(typeName) } - val label = Label(rs.getString("label"), type, rs.getInt("color")) + val label = Label(rs.getString("label"), type, rs.getInt("color"), rs.getInt("ord")) label.id = rs.getLong("id") return label } + override fun save(label: Label) { + config.getConnection().use { connection -> + if (label.id != null) { + connection.prepareStatement("UPDATE Label SET label=?, type=?, color=?, ord=? WHERE id=?").use { ps -> + ps.setString(1, label.toString()) + ps.setString(2, label.type?.name) + ps.setInt(3, label.color) + ps.setInt(4, label.ord) + ps.setInt(5, label.id as Int) + ps.executeUpdate() + } + } else { + try { + connection.autoCommit = false + var exists = false + connection.prepareStatement("SELECT COUNT(1) FROM Label WHERE label=?").use { ps -> + ps.setString(1, label.toString()) + val rs = ps.executeQuery() + if (rs.next()) { + exists = rs.getInt(1) > 0 + } + } + + if (exists) { + connection.prepareStatement("UPDATE Label SET type=?, color=?, ord=? WHERE label=?").use { ps -> + ps.setString(1, label.type?.name) + ps.setInt(2, label.color) + ps.setInt(3, label.ord) + ps.setString(4, label.toString()) + ps.executeUpdate() + } + } else { + connection.prepareStatement("INSERT INTO Label (label, type, color, ord) VALUES (?, ?, ?, ?)").use { ps -> + ps.setString(1, label.toString()) + ps.setString(2, label.type?.name) + ps.setInt(3, label.color) + ps.setInt(4, label.ord) + ps.executeUpdate() + } + } + connection.commit() + } catch (e: Exception) { + connection.rollback() + throw e + } + } + } + } + override fun countUnread(label: Label?): Int { val where = if (label == null) { "" @@ -68,7 +113,7 @@ class JdbcMessageRepository(private val config: JdbcConfig) : AbstractMessageRep try { config.getConnection().use { connection -> connection.createStatement().use { stmt -> - stmt.executeQuery("SELECT count(*) FROM Message WHERE $where").use { rs -> + stmt.executeQuery("SELECT count(1) FROM Message WHERE $where").use { rs -> if (rs.next()) { return rs.getInt(1) } @@ -127,7 +172,7 @@ class JdbcMessageRepository(private val config: JdbcConfig) : AbstractMessageRep val result = ArrayList<Label>() try { connection.createStatement().use { stmt -> - stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE $where").use { rs -> + stmt.executeQuery("SELECT id, label, type, color, ord FROM Label WHERE $where").use { rs -> while (rs.next()) { result.add(getLabel(rs)) } @@ -226,11 +271,15 @@ class JdbcMessageRepository(private val config: JdbcConfig) : AbstractMessageRep ps.setObject(13, message.nextTry) ps.setObject(14, message.conversationId) - ps.executeUpdate() - // get generated id - ps.generatedKeys.use { rs -> - rs.next() - message.id = rs.getLong(1) + try { + ps.executeUpdate() + // get generated id + ps.generatedKeys.use { rs -> + rs.next() + message.id = rs.getLong(1) + } + } catch (e: SQLIntegrityConstraintViolationException) { + throw AlreadyStoredException(cause = e) } } }