344 Commits

Author SHA1 Message Date
chris 519f457476 🐛 Fix connectivity issues and improve code 2018-10-15 19:57:01 +02:00
chris fe9fa0ba2f 🐘 Add settings to reduce memory usage
* NioNetworkHandler lets you tweak the minimum number of connections
* BufferPool can now set a limit to how many Buffers it retains.
  This one might still need some tweaking.
2018-06-12 07:00:52 +02:00
chris 37cda3df56 🔥 Remove duplicate code 2018-05-31 22:33:26 +02:00
chris fafabf64a3 🔥 Remove duplicate code 2018-05-31 16:58:29 +02:00
chris 7b9694e660 😴 Improve code quality 2018-05-31 07:32:07 +02:00
chris ce86ab55c3 ⬆ JUnit version bump 2018-05-30 22:08:53 +02:00
chris 25e118b88e 🔥 Remove unnecessary methods 2018-05-30 17:01:05 +02:00
chris cbebc38579 Improve code quality 2018-05-29 21:00:25 +02:00
chris b44a2f8809 🚀 Improve performance 2018-05-25 20:47:40 +02:00
chris c7c285a2c1 Improve conversation class
* make it serializable
* provide either first unread or last message as extract
2018-03-12 19:41:56 +01:00
chris 81fc50ec37 Improvements for working with conversations 2018-03-05 10:12:21 +01:00
chris f1403bcd00 Bumped Gradle wrapper and Kotlin dependency 2018-03-05 09:50:24 +01:00
chris e9acb0071e Make network handler more robust 2018-02-21 07:44:53 +01:00
chris c425298b67 Use JUnit 5 for tests, bump dependencies 2018-02-20 14:04:40 +01:00
chris 681ea148db Allow missing recipient for drafts 2018-02-16 17:04:24 +01:00
chris fab1c06135 Refactor to utilize Kotlin features 2018-02-16 17:04:08 +01:00
chris b93f382ccd Bump Kotlin to version 1.2.21 2018-02-16 17:03:37 +01:00
chris 00e4461043 Fix issues with Java 7 compatibility 2018-02-16 16:57:08 +01:00
chris 18f870a4cc Switched from commons-lang3 to commons-text 2017-11-27 21:46:19 +01:00
chris 278d5b05e6 Split LabelRepository off the MessageRepository 2017-11-26 20:30:05 +01:00
chris ddb2073c2f Refactored BitmessageContext creation 2017-11-25 20:34:11 +01:00
chris a5c78fd8cf Renamed InputStreams from in and is to input, so it doesn't look strange in kotlin 2017-11-24 07:37:04 +01:00
chris 8cbdce6eac Refactored to use StreamableWriter
Bumped the msgpack library to 2.0.1 (the 2.0.0 build was fubar)
2017-11-21 10:44:41 +01:00
chris ece9cd8667 Improved the labeler and bumped the msgpack library to 2.0.0 2017-09-23 08:22:34 +02:00
chris bf0c946c52 Different improvements
- catch all kinds of errors when selector binding fails
- pimped DefaultLabeler to support listeners
- added pagination capabilities to AbstractMessageRepository
2017-09-13 08:06:06 +02:00
chris 273d229709 Merge branch 'feature/exports' into develop 2017-09-13 07:55:11 +02:00
chris c8dfc3b459 Added option to save labels
and other improvements and fixes used for exports and imports
2017-08-25 21:11:10 +02:00
chris cf6b3e2603 Added easy way to disable acknowledges and fixed possible issue with builder-constructor 2017-08-11 17:35:46 +02:00
chris c81c89197b Very minor code improvements 2017-08-11 12:32:20 +02:00
chris 6e79b0c50f 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)
2017-07-29 14:56:03 +02:00
chris d9e52c85c3 Fixed occasional on the selector - it shouldn't crash the application 2017-07-22 08:01:59 +02:00
chris 6c04aa683e Added exports for messages, labels and adddresses
and some code improvements
2017-07-21 10:27:20 +02:00
chris fd08fa3883 Fixed build 2017-07-16 21:25:12 +02:00
chris 3e286c08b4 Fixed typo 2017-07-15 19:56:14 +02:00
chris 644dcc692f Minor improvements 2017-07-15 19:41:20 +02:00
chris 009346cd30 Some improvements for Android compatibility 2017-07-10 06:08:20 +02:00
chris 1a33f744d6 Some improvements for Java interoperability 2017-07-08 19:53:29 +02:00
chris 0478431c9c Merge branch 'feature/kotlin' into develop 2017-07-07 22:12:54 +02:00
chris d3a06e7639 Added version to user agent info, and other minor fixes 2017-07-05 00:37:45 +02:00
chris 35d7486869 Added dependency management and bumped gradle version 2017-07-02 11:46:27 +02:00
chris a8addf946b Minor improvements and version bumps 2017-07-02 08:37:43 +02:00
chris a245288359 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
2017-06-28 00:01:05 +02:00
chris aee5debdd2 Fixed system test and added some fixes for Java backwards compatibility 2017-06-27 17:22:48 +02:00
chris 322bddcc4f Migrated networking and fixed networking tests 2017-06-25 20:06:17 +02:00
chris 894e0ff724 Fixes and improvements, SystemTest still broken 2017-06-16 07:03:12 +02:00
chris 1d3340a547 Migrated cryptography and some improvements 2017-06-08 21:56:31 +02:00
chris 83e50e1ad1 Migrated BitmessageContext and fixed some tests 2017-06-08 16:59:24 +02:00
chris fa0e53289c Migrated core and extension modules to Kotlin
(Except BitmessageContext and Bytes)
2017-06-06 16:36:07 +02:00
chris 811625c051 Fixed write(ByteBuffer) 2017-05-19 20:12:21 +02:00
chris 869d2e0386 Fixed pubkey retrieval
Broadcast subscriptions shouldn't be disabled every time a pubkey, message or broadcast comes along... (this might also fix other strange behaviour)
2017-05-19 19:49:35 +02:00
chris 239c6ec7f4 Fixed NullPointerException 2017-05-16 17:20:52 +02:00
chris 956ed61b14 Removed unused logger 2017-05-08 07:26:45 +02:00
chris c4b26cac1c Merge branch 'feature/extended-encoding' into develop 2017-05-04 07:27:52 +02:00
chris 2ae1e561d8 Minor improvements for logging and debugging 2017-05-04 07:24:26 +02:00
chris e5c956c6e5 Minor improvements 2017-04-12 17:18:09 +02:00
chris f50d7445c1 Validate TTL: it must always be more than zero and less than 28 days 2017-04-12 17:12:43 +02:00
chris 95c9be4d1c A slightly more elegant way to define the plugin dependency 2017-04-04 07:31:37 +02:00
chris dca0330a7c Bumped dependency versions 2017-04-03 18:21:08 +02:00
chris 7185acbbad Bumped gradle version to 3.4.1 2017-04-02 23:27:31 +02:00
chris c4385b2336 Add some passive support for SHA256 based signatures 2017-04-02 21:03:04 +02:00
chris 841fb7eccd Test fixes and improvements 2017-04-02 21:02:20 +02:00
chris 016b4f80ba Code fix 2017-03-31 17:07:57 +02:00
chris 5849e68d20 Keep order of labels 2017-03-30 16:31:36 +02:00
chris 3ab3d7a0ca ConversationService additions, fixed migration script 2017-03-30 16:30:46 +02:00
chris d9090eb70c Some code to work with conversations 2017-03-13 22:49:40 +01:00
chris 10a45cc79c Moved to own MsgPack implementation 2017-02-03 07:29:51 +01:00
chris 3f8980e236 Make sure context is set after it is properly initialized 2017-01-11 17:30:04 +01:00
chris 732032b1b5 Fixed some bugs and some tests 2016-12-21 08:09:53 +01:00
chris 702ac6cb82 Added some null checks so the users of this library don't have to 2016-12-16 07:30:02 +01:00
chris 0d67701735 Changed default CustomCommandHandler
While throwing an IllegalStateException is great to signal developers they need to register a default handler, it's a dangerous default for all those who don't need one. A log entry on 'debug' level should suffice.
2016-12-12 07:59:38 +01:00
chris c11a1b78c4 Some code style improvements 2016-12-11 14:27:21 +01:00
chris 6d67598a40 First implementation of extended message encoding. Works as far as the PyBitmessage implementation, with some additional code. 2016-12-11 14:02:32 +01:00
chris e1dcbbf19c Don't block when adding a new contact / searching for its public key 2016-12-07 20:01:19 +01:00
chris 31eca20cca Fixed NPE when 'from' doesn't have a public key. From will get lost in those cases unless it's saved separately. 2016-12-05 07:41:55 +01:00
chris 0bb455d433 Extended encoding basics
works for basic subject/body messages, no attachments and no other features supported yet
2016-11-30 17:26:22 +01:00
chris 831e4bcbcc Added clarifying comment 2016-11-14 23:27:29 +01:00
chris df7f03d81a Merge tag '2.0.4' into develop
Version 2.0.4
2016-11-01 06:29:00 +01:00
chris 64ee41aee8 Merge branch 'release/2.0.4' 2016-11-01 06:28:44 +01:00
chris d3205336ed Some tweaks to determine what objects should be requested from other nodes, so they may arrive as quickly as possible. 2016-10-31 06:21:36 +01:00
chris 7b14081c63 Set 'send time' on outgoing messages 2016-10-28 07:52:05 +02:00
chris e1173d0619 Merge tag '2.0.3' into develop
Version 2.0.3
2016-10-24 08:13:18 +02:00
chris f0a5a40edd Merge branch 'release/2.0.3' 2016-10-24 08:12:48 +02:00
chris 1bc82cdd7d Merge branch 'master' into release/2.0.3 2016-10-22 09:37:56 +02:00
chris a880a8c10b Fixed NPE 2016-10-22 07:24:49 +02:00
chris 6a5fe01860 Merge tag '2.0.2' into develop
Version 2.0.2
2016-10-15 18:02:18 +02:00
chris 5cf6d308f2 Merge branch 'release/2.0.2' 2016-10-15 18:02:06 +02:00
chris ad97cd0633 Fixed SecurityException for some Android versions.
At the same time removed necessity to register a cryptography provider which means SpongyCryptography can be used on the Oracle JVM as well - but this is something vor Jabit 3.0.
2016-10-15 18:01:08 +02:00
chris 5043e9ed03 Merge tag '2.0.1' into develop
Version 2.0.1
2016-10-07 22:10:16 +02:00
chris 15c6540e16 Merge branch 'release/2.0.1' 2016-10-07 22:10:00 +02:00
chris 784ed9ed4e Fixed importer exception on Android 2016-10-07 22:08:55 +02:00
chris 3a0555e6e9 Merge tag '2.0.0' into develop
Version 2.0.0
2016-10-02 23:26:20 +02:00
chris e71f30736d Merge branch 'release/2.0.0' 2016-10-02 23:22:34 +02:00
chris 503e665c5b Updated README.md 2016-10-02 23:16:18 +02:00
chris 579d604ac6 getRemoteAddress doesn't work on Android (at least not KitKat), so let's get the address this way. 2016-09-26 17:38:50 +02:00
chris 1003e7a582 Merge branch 'feature/nio' into develop 2016-09-21 19:38:48 +02:00
chris a18f76f864 Cleaning up requested objects from time to time, to work around a leak that sometimes happens. 2016-09-21 19:37:17 +02:00
chris 7e201dd2cf Change version of 'develop' branch to 'development-SNAPSHOT' 2016-09-13 07:36:28 +02:00
chris 83abce0f52 Deprecate DefaultNetworkHandler in favor of NioNetworkHandler 2016-09-13 07:25:23 +02:00
chris e6d40cde76 Try to fix tests on travis.
Unfortunately they work fine locally, so there may be several of those attempts.
2016-09-12 11:15:07 +02:00
chris 71124d7b01 Fixed tests 2016-09-12 10:02:52 +02:00
chris 489b8968e0 Refactored use of the DefaultMessageListener so it's retrieved from the InternalContext 2016-09-12 08:18:30 +02:00
chris a240606909 Minor improvements and fixes 2016-09-05 19:35:36 +02:00
chris dad05d835b Created an improved JdbcNodeRegistry and removed MemoryNodeRegistry, as it doesn't properly work with the way nodes are handled and disseminated in the new PyBitmessage client. The new one should work a lot more stable. 2016-09-01 07:35:46 +02:00
chris 827973f642 Improved connecting to the network 2016-08-29 12:30:26 +02:00
chris 53aa2c6804 Slightly improved the problem with stale objects in the requested objects list. But I don't know how to properly solve it, there may always be some left. 2016-08-25 12:44:06 +02:00
chris 102d63e2c6 Fixed message count per label 2016-08-25 08:50:06 +02:00
chris caa2219a63 It looks like the NIO network handler works now - some testing needed to see how reliably 2016-08-24 22:17:02 +02:00
chris 3a92bab9ba Merge remote-tracking branch 'origin/develop' into feature/nio 2016-08-17 07:46:52 +02:00
chris 1eac644813 Fixed error in AbstractMessageRepository (archived messages couldn't be found) 2016-08-15 11:52:43 +02:00
chris 41c4447514 Automatically set version from git
- Uses last tag on master branch (this is set on 'git flow release finish')
- branch name snapshot otherwise, e.g. 'feature-nio-SNAPSHOT', 'develop-SNAPSHOT'

(cherry picked from commit 86cfc66a40)
2016-08-12 22:02:06 +02:00
chris 631e71bc74 Updated gradle
(cherry picked from commit 52422d3)
2016-08-12 21:58:36 +02:00
chris 86cfc66a40 Automatically set version from git
- Uses last tag on master branch (this is set on 'git flow release finish')
- branch name snapshot otherwise, e.g. 'feature-nio-SNAPSHOT', 'develop-SNAPSHOT'
2016-08-12 16:27:57 +02:00
chris 52422d3398 Updated gradle 2016-08-12 11:04:04 +02:00
chris cd3a801704 Used wrong nonce for version message 2016-08-09 19:50:11 +02:00
chris 505818a712 Added option to connect to local Bitmessage client
(This makes it easier to debug some problem or make some tests)
2016-08-08 18:00:50 +02:00
chris 92229151a5 It seems travis is a bit slow sometimes, so I raised the timeout for the NetworkHandlerTest 2016-07-29 11:47:05 +02:00
chris 334a510743 Fixes, improved tests and other improvements 2016-07-29 07:49:53 +02:00
chris 56ebb7b8fa Better memory management for the out buffer 2016-07-27 07:38:39 +02:00
chris 48ff975ffd Better memory management for the in buffer (the same TODO for the out buffer. 2016-07-25 07:52:27 +02:00
chris 82ee4d05bb Raised test timeout, as the Jacoco run seems to be considerably slower. 2016-07-10 06:55:24 +02:00
chris 50f2c7e080 Fixed synchronisation 2016-07-09 16:37:12 +02:00
chris d130080df2 Implemented methods offer and request, system test works now but synchronization is still broken. 2016-07-08 18:14:41 +02:00
chris abc2f63aa6 Some further fixes and improvements, not all tests working yet 2016-06-20 16:33:47 +02:00
chris ae2120675f Tests with NioNetworkHandler as peer work now 2016-06-18 23:09:23 +02:00
chris 0fadb40c6c Improved tests and fixed some 2016-06-16 19:47:59 +02:00
chris ed4fd1002b Improved uint reading 2016-06-12 20:53:05 +02:00
chris 62d40fb2c3 Improved unsigned byte comparison 2016-06-12 20:43:23 +02:00
chris 12fb794203 Minor test improvements 2016-06-09 17:46:21 +02:00
chris cde4f7b3ce Some refactoring to move some common code into an AbstractConnection 2016-06-01 17:38:49 +02:00
chris 425a9dd6bf Merge branch 'develop' into feature/nio 2016-05-28 11:05:39 +02:00
chris c1fa642b4e Made tests more stable, albeit slightly slower 2016-05-28 11:04:47 +02:00
chris 08f2d5d6f1 Added write(ByteBuffer) to Streamable interface and a first draft for a NioNetworkHandler 2016-05-28 10:22:47 +02:00
chris b8f88b02d1 Improved tests 2016-05-26 22:50:37 +02:00
chris 5c4892d153 Added test 2016-05-26 06:55:31 +02:00
chris 3d2cea91ce Merge branch 'feature/ACK' into develop 2016-05-24 19:36:41 +02:00
chris 22108527f3 Minor update to the README file 2016-05-24 19:35:41 +02:00
chris 725d2b848e Fixed migration and added resend and cleanup options to demo application 2016-05-24 17:19:29 +02:00
chris 409dccd0be Fixed broken JavaDoc and removed unused import 2016-05-24 07:45:34 +02:00
chris ed6344c662 Added BitmessageContext method to resent unacknowledged messages and updated README.md 2016-05-23 20:11:44 +02:00
chris 14849a82ea Refactored JdbcMessageRepository so that alternative implementations can be done easier 2016-05-20 23:58:08 +02:00
chris c3d8a07e83 Added unit tests and fixed bug 2016-05-20 23:00:27 +02:00
chris 43f42dd400 This breaks a lot of things, but it seems necessary. Implemented the resending mechanism and fixed many problems on the way, but tests and triggers are still to do. 2016-05-20 07:32:41 +02:00
chris e44dd967d0 Test for NodeRegistry 2016-05-13 12:25:04 +02:00
chris a67ac27921 Fixed yet another test 2016-05-10 07:26:25 +02:00
chris 05d9ea93d2 Acknowledgments are now returned, received, and the message (Plaintext object) updated
-> no logic to resend messages yet
2016-05-06 19:39:39 +02:00
chris de8f04e22a Added warning to Labeler for developers who want to implement it. 2016-05-06 17:29:39 +02:00
chris 4f0b2cb8f8 Fixed system test (this time for real) 2016-05-06 14:13:39 +02:00
chris 678a48ac3f Fixed system test and ProofOfWorkService 2016-05-05 10:50:22 +02:00
chris c7594795f0 Fixed tests & bugs, removed Ack payload type (a GenericPayload is now used)
- SystemTest don't work yet, sending messages seems broken
- ProofOfWorkService needs some work, the current solution is a hack (and might be the reason above tests are broken)
2016-05-02 11:11:29 +02:00
chris ea2cd7bf53 Improved labeler to cover all cases, and fixed when labels are set while sending (outbox vs sent)
Removed message callback with both didn't work and is obsolete (use a labeler descendant)
2016-04-29 15:29:22 +02:00
chris 8df42a6cf0 Merge branch 'develop' into feature/ACK
# Conflicts:
#	core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java
2016-04-28 19:32:21 +02:00
chris 784b192bab Simplyfied MultiThreadedPOWEngine and thread pool creation 2016-04-28 07:15:48 +02:00
chris a0505f5704 Minor improvements to the demo Application and a fix for when the ACK is empty 2016-04-25 08:13:46 +02:00
chris 61890b3da9 Some improvements to the application 2016-04-15 17:18:11 +02:00
chris ddd5826f42 Fixed feature bitfield calculation/resolution 2016-04-13 07:32:35 +02:00
chris c31ec7a9e5 Fixed problems after merging 2016-04-12 17:27:19 +02:00
chris 6f4f51aef3 Merge branch 'develop' into feature/ACK
# Conflicts:
#	core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java
#	core/src/main/java/ch/dissem/bitmessage/InternalContext.java
#	core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java
#	core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java
2016-04-11 23:41:23 +02:00
chris e8ddf90363 Provide a more flexible way to label messages.
I'm not quite sure about chans yet
2016-04-11 15:10:59 +02:00
chris 4f7f80c12a Added tests and code improvements 2016-04-08 19:22:40 +02:00
chris f70c15da38 Some code for deterministic addresses - needs tests 2016-04-07 23:01:16 +02:00
chris 32ea3517fe Chans should now work.
Other deterministic addresses should be easy to implement, but aren't done yet
2016-04-07 17:24:56 +02:00
chris ead5341b2e Some code for supporting chans 2016-03-31 20:04:23 +02:00
chris 3e5e431d6f Code cleanup 2016-02-28 23:03:00 +01:00
chris 57057298a1 Code cleanup 2016-02-26 16:30:45 +01:00
chris 9ca28ead66 Code cleanup 2016-02-26 16:12:43 +01:00
chris 2a17e6024f Code cleanup 2016-02-26 15:06:47 +01:00
chris bc68a5d3ec Code cleanup & improvements
- most notably removed some unnecessary synchronize blocks in the DefaultNetworkHandler
2016-02-26 14:34:08 +01:00
chris 382cb80a87 Updated badges 2016-02-26 14:01:26 +01:00
chris f6add5b2ea Code cleanup 2016-02-25 16:36:43 +01:00
chris 0a00a0a1b4 Code cleanup 2016-02-24 23:25:55 +01:00
chris 9f1e0057c9 Code cleanup 2016-02-24 23:10:04 +01:00
chris 4dd639e651 Code cleanup 2016-02-24 22:51:35 +01:00
chris f5215be8c6 Merge remote-tracking branch 'origin/master' into develop
# Conflicts:
#	README.md
2016-02-24 19:43:21 +01:00
chris a72718d4e8 Update README.md 2016-02-24 16:53:11 +01:00
chris 8b2133977c Merge pull request #20 from Erkan-Yilmaz/patch-1
typo
2016-02-24 16:21:07 +01:00
Erkan Yilmaz f17f26bf34 typo 2016-02-24 15:14:42 +01:00
chris 86accb94f2 Update README.md
added link to IRC channel (thanks, kiwiirc!)
2016-02-24 14:34:14 +01:00
chris ccb102efd7 Update README.md 2016-02-15 16:54:13 +01:00
chris f71671e04a Added tests for DefaultMessageListener and ProofOfWorkService
and some minor improvements
2016-02-15 07:33:38 +01:00
chris e4a69f42b0 Fixed problem with sending broadcasts
(while adding some tests)
2016-02-13 08:03:05 +01:00
chris af3e63f592 Improved tests for cryptography 2016-02-09 17:09:22 +01:00
chris 60adf73616 Improved tests for repositories 2016-02-07 23:36:35 +01:00
chris 9c375d6608 Update README.md 2016-02-06 17:27:12 +01:00
chris db64b55510 Version 1.0.2-SNAPSHOT bump
Fixed NPE if you create a Plaintext object without recipient
2016-02-06 15:47:13 +01:00
chris 06dbfbf64a Update README.md 2016-02-04 13:01:03 +01:00
chris 58e9644ff1 Merge tag '1.0.1' into develop
1.0.1 1.0.1
2016-02-04 10:18:40 +01:00
chris d580d6983b Merge branch 'release/1.0.1' 2016-02-04 10:18:39 +01:00
chris 9231cf5eaa Skip system tests by default as they don't work on Travis CI 2016-02-04 10:12:43 +01:00
chris 354f506872 Raised timeout for test so hopefully it doesn't fail anymore on Travis CI (otherwise there must be a different problem) 2016-02-03 19:28:41 +01:00
chris 2bfeedc7a9 Update build.gradle
Another try for codecov.io
2016-02-03 17:32:04 +01:00
chris ea700755b6 Update .travis.yml
Another try
2016-02-03 17:17:31 +01:00
chris 5ab577f18a Update .travis.yml to work with codecov.io 2016-02-03 17:09:26 +01:00
chris b1599cbd60 This should fix the build on Travis CI / machines that don't have signing configured 2016-02-03 07:52:22 +01:00
chris 91c41fa3bd Version 1.0.1 bump 2016-02-02 21:23:15 +01:00
chris 985e830779 Made TTLs easily changeable (albeit not for specific messages)
This should make the system test run on Travis CI again
2016-02-02 21:05:14 +01:00
chris edd8045327 Fixed / improved message sending and added a system test (it's a start) 2016-02-02 20:40:01 +01:00
chris 5f4dbfc985 Version 1.0.1-SNAPSHOT - fixed issue with requesting pubkey, and problem where your keys are overwritten if you try to import a contact again or worse, your identity as a contact 2016-01-31 18:11:20 +01:00
chris 3103ae6edd Merge remote-tracking branch 'origin/master' into develop 2016-01-29 18:20:24 +01:00
chris 1a77396bdc Update README.md
Fixed error where bc and sc was used instead of bouncy and spongy
2016-01-29 11:06:39 +01:00
chris 3eabda29ee Merge branch 'master' into develop 2016-01-24 09:38:47 +01:00
chris 8bd7a245b0 Updated README.md 2016-01-24 09:38:12 +01:00
chris ae8a0ac0b9 Merge tag '1.0.0' into develop
Tagging version 1.0.0 1.0.0
2016-01-23 17:22:05 +01:00
chris e56d8c25e0 Merge branch 'release/1.0.0' 2016-01-23 17:22:03 +01:00
chris 9a91351091 Version 1.0.0 bump 2016-01-23 17:21:33 +01:00
chris 6c0eae5919 Added CONTRIBUTING.md 2016-01-23 17:19:36 +01:00
chris 07b349563f Fixed an issue in the POW engine 2016-01-23 17:18:25 +01:00
chris 9f05af8bb7 Finally fixed the bug that was haunting me for the last week. 2016-01-21 20:32:23 +01:00
chris 733335ef42 Improved performance and network stability 2016-01-19 21:09:46 +01:00
chris e29310102f Updated UML diagram 2016-01-19 21:07:26 +01:00
chris 35077243b0 Fixed synchronization 2016-01-17 07:13:29 +01:00
chris ac6f291964 Renamed module 'domain' to 'core' to make its purpose more clear 2016-01-17 05:42:49 +01:00
chris 8764642878 Major fixes and improvements to the network module, fixing problems where objects where requested multiple times or not at all in some situations. 2016-01-16 08:47:50 +01:00
chris 549c8854ed Refactoring: renamed 'security' to 'cryptography' 2016-01-10 13:38:32 +01:00
chris de0100e14f Merge branch 'feature/server-pow' into develop 2016-01-10 12:22:57 +01:00
chris fad3e07871 Some changes needed for POW server and some general improvements 2015-12-21 15:13:48 +01:00
chris 61788802c5 Some POW improvements 2015-12-18 16:42:17 +01:00
chris 51bf3b8bd2 Fixed tests 2015-12-12 11:05:13 +01:00
chris ab6a3c56dd The POW callback is now a service and its state stored.
The proof of work engine therefore just has to remember its initial hash making server based POW easier.
2015-12-08 20:27:32 +01:00
chris 991a0e5f86 Some improvements for custom message handling 2015-12-02 17:45:50 +01:00
chris 99266712fa Some extensions for server POW 2015-11-28 20:27:05 +01:00
chris 2fae90c433 Some code for sending acknowledgements
- some of it isn't tested
- somehow the ack part seems to be empty, even though the flag should be set
2015-11-08 19:29:26 +01:00
chris 1f05a52f05 Improvements
- Massively reduced logging, especially at debug level
- Optimizations to reduce system load
- Use bootstrapping to find stable nodes
2015-11-08 10:14:37 +01:00
chris 2a8834e3c6 Timeout might have been too short for Travis CI
(or there is an actual problem with concurrency)
2015-10-30 20:05:55 +01:00
chris c9c0806e0d Attempt to disconnect on thread interrupt 2015-10-29 12:34:29 +01:00
chris 9c2d8589bf Improved POW test 2015-10-28 16:56:09 +01:00
chris b496f81b20 Fixed POWEngine and improved test.
(Locks can't be released from a different thread, we need to use a semaphore)
2015-10-27 07:50:20 +01:00
chris 36fe780766 The nonce is now set over a callback method in the POW engine. This should make some POW implementations easier. 2015-10-26 09:49:49 +01:00
chris bdc8e025c1 Connections are now severed after a configurable time (12h by default) or when a limit is exceeded (150 by default) 2015-10-24 12:08:23 +02:00
chris a398b072b5 (probably) fixed another concurrency problem in the tests 2015-10-19 15:14:25 +02:00
chris fb300c8731 Fixed possible ConcurrentModificationException 2015-10-19 15:08:11 +02:00
chris 1e605f56a5 Added method to retrieve all properties 2015-10-18 18:22:49 +02:00
chris 3f1b41a2c1 Merge branch 'develop' of github.com:Dissem/Jabit into develop 2015-10-15 18:04:55 +02:00
chris ac70a4b632 We probably shouldn't leave the bitmessage node running after the test is finished 2015-10-15 17:59:32 +02:00
chris 4913a21b11 Fixed NetworkHandlerTest 2015-10-15 15:34:04 +02:00
chris d39342b12f Update .travis.yml
Travis seems to use Java 7 by default, probably because it's defined in the base `build.gradle`. This causes a problem for modules that use Java 8.
2015-10-15 08:21:41 +02:00
chris 409100ab20 Work-around for parsing problem
(and made code more robust for different parsing problems)
2015-10-14 22:56:46 +02:00
chris ddaa52f416 Merge branch 'feature/threaded-connections' into develop 2015-10-14 20:38:37 +02:00
chris 511b3c1754 Connections now use two separate threads for writing and listening
- this should avoid dead locks, specifically when connecting to Jabit :/
- also, Java 8 features are now allowed in modules not needed by Android clients
2015-10-14 18:37:43 +02:00
chris 117ac3ca73 Fixed some problems and added cleanup on shutdown 2015-10-12 12:44:13 +02:00
chris 3d1bd7227b Updated H2 version 2015-10-12 12:42:11 +02:00
chris ea1419eda1 Synchronisation API - added option to wait for the synchronization to finish 2015-10-08 13:12:39 +02:00
chris f9ff22bebe Synchronisation API and related refactorings / improvements
-> lets you synchronize with the Bitmessage network without staying connected
2015-10-07 21:50:41 +02:00
chris c3fdee79ca Some bugfixes and added findMessages by sender 2015-09-29 07:13:27 +02:00
chris 7fb837645f Synchronisation now shouldn't fail if the trusted host has no new messages
- fixed tests for Gradle builds
- fixed SerializationTest
2015-09-25 23:35:31 +02:00
chris d67c932fb2 Added synchronization code and unit test.
Synchronisation fails if the trusted host has no new messages - this needs to be fixed (but shouldn't be an issue for real world applications)
2015-09-24 08:09:20 +02:00
chris f89d1a342e Fixed a few problems:
- some bugs that creeped in when I moved security into its own adapter
- improved some DB code as it doesn't work in Android anyway
- all entities should be serializable (very useful in Android)
2015-08-28 13:48:01 +02:00
chris 4911c268c2 Changed repositories to work with SQLDroid, which seems to have very limited support for blobs, at least it didn't work when I used stream. 2015-08-05 19:55:53 +02:00
chris b8546e28af Moving "Security" to a separate port, so there can be a Bouncycastle and a Spongycastle implementation. (BC doesn't work on Android, SC can't be used on Oracle's JVM) 2015-08-05 19:52:18 +02:00
chris 6542bd1451 now the build should work for anyone (esp. travis) 2015-07-04 11:13:35 +02:00
chris 070cde699e Version 0.2.1-SNAPSHOT bump - minor changes so anyone should be able to build and I can easily upload to maven central 2015-07-04 10:26:02 +02:00
chris bef9ea716e Merge branch 'release/0.2.0' into develop 2015-07-03 14:48:34 +02:00
chris c557eafc36 Merge branch 'release/0.2.0' 2015-07-03 14:48:33 +02:00
chris c1ca1d856d Version 0.2.0 bump 2015-07-03 14:47:25 +02:00
chris 6fbb3f850d There are some instances where an inventory item is requested and therefore saved twice. This shouldn't be logged as an error. 2015-07-03 14:46:55 +02:00
chris 6f50c200ee Added import/export to the demo app
- discovered private key length was wrong - fixed
- as things are broken anyway, refactored flyway migrations - you'll need to delete ~/jabit.*.db
2015-07-03 11:28:06 +02:00
chris 65fdd7d408 Dropped obsolete table and set lower POW target for test (there is no need) 2015-07-03 09:04:35 +02:00
chris 7f4c67f43e Removing IV of received objects from requested objects list (I forgot that one) 2015-07-03 07:56:18 +02:00
chris 2c4d95af2f smarter network code, fixed various issues
- deciding on the stream at creation time is just silly - it should be done based on the identities (this part is TODO)
- changed NodeRegistry so it doesn't store nodes - this should help to connect faster
- inventory items shouldn't be advertised to nodes that are already aware of them (issue #13)
- objects shouldn't be requested more than once (issue #9)
2015-07-01 06:57:30 +02:00
chris c8960df2b3 issues #11 and #12 - import and export for Wallet Import Format 2015-06-27 07:52:28 +02:00
chris 884171fe18 issue #15: fixed socket timeouts and connection shutdown - no exceptions should be thrown if the network node is being shut down, although it can take up to 5 seconds = max(READ_TIMEOUT, CONNECT_TIMEOUT) 2015-06-24 22:56:17 +02:00
chris fb25852bac Removed docs into another repo - they don't have much to do with Jabit
(If you must: https://git.dissem.ch/chrigu.meyer/thesis/)
2015-06-24 06:55:25 +02:00
chris b150d08188 Fixed NPE 2015-06-23 22:29:44 +02:00
chris 3b3e972c99 This should fix the missing ossrhUsername problem 2015-06-21 09:02:03 +02:00
chris fefefe6809 Added some stuff necessary to publish to maven central - I hope it still builds on travis 2015-06-21 08:58:49 +02:00
chris 607e777a7a Version 0.1.3-SNAPSHOT bump 2015-06-20 10:00:47 +02:00
chris 20aeb8ec48 Merge branch 'develop'
Conflicts:
	build.gradle
2015-06-20 08:11:25 +02:00
chris 027efe43df Version 0.1.2 bump - fixed problem where IPv4 Addresses weren't converted correctly. 2015-06-20 08:09:51 +02:00
chris 455c0b9c2c Update README.md 2015-06-18 22:17:38 +02:00
chris 9de4f7a822 Revert "Version 0.1.2-SNAPSHOT bump"
This reverts commit 0b58362054.
2015-06-18 22:00:43 +02:00
chris 0b58362054 Version 0.1.2-SNAPSHOT bump 2015-06-18 21:59:41 +02:00
chris b6f42c2f3d Version 0.1.1 bump - fixed unit test / DB initialisation error 2015-06-18 21:53:35 +02:00
chris 72d0fbe550 Version 0.1.0 bump
Updated Gradle file structure so the version and a few other things are only defined once
2015-06-18 21:26:08 +02:00
chris 95efbd0235 Added documentation 2015-06-18 18:26:10 +02:00
chris 1eb6851798 Updated and fixed JavaDoc 2015-06-18 18:25:47 +02:00
chris 6be8d51f6d Distributable JAR build
- connection manager should now be rock stable
- does try to create new connections as long as there are less than eight active connections, which might result in more than eight outgoing connections, but this shouldn't be a problem
- some minor improvements and bug fixes
2015-06-18 13:41:11 +02:00
chris 1dc4582012 Issue # 5: fixed test and initialisation 2015-06-17 06:23:00 +02:00
chris 9b0de83706 Issue # 5: initialize node repository if there are no nodes in DB 2015-06-17 06:00:03 +02:00
chris 49a9e0c5f7 Fixed test 2015-06-16 07:08:33 +02:00
chris c0a7acc609 Greatly improved network code - the "manage the node repository" part of issue #5 should now be OK 2015-06-16 06:41:59 +02:00
chris ed0d1c2911 issue #4: prevent connections to self, select random nodes to connect to 2015-06-13 17:37:55 +02:00
chris bd5bf76904 Improved connection management, preventing multiple connections to the same node, and improved broadcast handling. 2015-06-12 06:57:20 +02:00
chris fe93c95f40 Added some improvements to the demo application 2015-06-11 21:02:01 +02:00
chris b40d2e9f73 Fixed issue #8 - search for MAGIC bytes instead of just checking
(NOW it should work)
2015-06-11 20:00:37 +02:00
chris a053b64fe6 Fixed issue #8 - search for MAGIC bytes instead of just checking 2015-06-11 19:42:44 +02:00
chris effb2ac2fb Fixing issue #4 - leading zeroes must be omitted on writing the coordinate components 2015-06-10 02:46:45 +02:00
chris 0566b27ce3 Fixed sending broadcasts 2015-06-10 01:02:03 +02:00
chris 35996019a2 There's something wrong with the V5 broadcast signature. I'm not quite shure what it is, so I made a test. 2015-06-10 00:49:44 +02:00
chris b4683bba68 Broadcasts. Receiving seems to work, but there still seems to be a problem with sending them. 2015-06-09 22:45:24 +02:00
chris f76864eebd Added tests for all repositories, fixed some bugs and made database configurable 2015-06-05 13:43:56 +02:00
chris 274c16b748 Implemented sending messages (and fixed a few bugs on the way)
This closes issue #3
2015-05-29 13:17:00 +02:00
chris 3d618ffeb4 Merge commit '01baf28cbdcee3563bad509caf419fdcbee8fb4d' 2015-05-27 19:08:36 +02:00
chris 593eebb33b Added some JavaDoc 2015-05-27 19:06:46 +02:00
chris 01baf28cbd Update README.md 2015-05-26 17:58:46 +02:00
chris c7542e438f Ignore this test - it's pointless anyway 2015-05-26 17:54:04 +02:00
chris 8cc1663273 added file .travis.yml 2015-05-26 17:48:46 +02:00
chris f0d4f3f81f Updated version to 0.0.1 (I don't know why 1.0 is default for new projects/modules) 2015-05-26 17:30:45 +02:00
chris ddf3574c43 Update README.md 2015-05-26 17:23:45 +02:00
chris 4883d8072e Updated documentation 2015-05-25 17:19:54 +02:00
chris a9081a8240 Updated documentation 2015-05-25 10:23:08 +02:00
chris 245ca22743 Updated documentation, added German translation 2015-05-24 21:55:37 +02:00
chris b347214b66 Introduced some custom exceptions 2015-05-23 16:01:18 +02:00
chris df4b67609e Updated documentation 2015-05-23 10:27:42 +02:00
chris b793526f2f Inventory items are now saved only if processing didn't fail. Receiving messages works, but there seems to be a problem with the POW check in some circumstances. 2015-05-23 10:27:05 +02:00
chris 6b3b361aa3 A simple command line application (WIP), and a few tests. Unfotrunately, receiving messages doesn't seem to work yet. 2015-05-22 20:51:57 +02:00
chris 648afbbc75 Update seminar.tex 2015-05-20 09:34:56 +02:00
chris 1fbc4a1d74 Major refactoring 2015-05-19 19:16:20 +02:00
chris b143225af5 Updated documentation 2015-05-16 16:34:03 +02:00
chris 07f185be35 Finally fixed address creation and private key import 2015-05-16 16:33:26 +02:00
chris cb1392e0f4 Signatures work 2015-05-15 15:25:06 +02:00
chris ed14e95d70 Signature check works 2015-05-15 12:36:57 +02:00
chris ee9c006794 Updated UML 2015-05-15 11:02:56 +02:00
chris bc630f82e2 Updated documentation 2015-05-15 10:59:56 +02:00
chris b996774ffb Some code cleanup 2015-05-12 19:37:42 +02:00
chris 46bb00c0aa I don't believe it - encryption works!!! 2015-05-12 19:04:00 +02:00
chris f23f432f07 Decryption works now!
(well, for v4 pubkeys at least)
2015-05-09 17:27:45 +02:00
chris d0250444d5 Docs 2015-05-08 13:15:29 +02:00
chris 5d6484cdf3 Pubkey import now works properly
(well theoretically - practically we still need client support to store them)
2015-05-06 20:36:42 +02:00
chris fde6398156 Fixed some stuff and broke some other - my goal is to solely use the java.security API 2015-05-05 20:48:42 +02:00
chris a65907f13b Some work on addresses and private keys that still doesn't work. As a side effect, sending objects now works basically. 2015-05-01 17:14:43 +02:00
chris 8d1466f6f4 Updated documentation 2015-04-30 20:34:42 +02:00
chris 2b1ca3467a Use 'content' as word for user-to-user messages and broadcasts 2015-04-29 20:13:43 +02:00
chris b104b84449 Documentation 2015-04-29 20:10:05 +02:00
chris 08c46b3a97 Some work on addresses, unfortunately it doesn't work yet. 2015-04-29 20:08:59 +02:00
chris 00bd6a08b7 Added SerializationTest for object payload (which is probably the most important) 2015-04-28 18:36:07 +02:00
chris 407c432f3c More documentation 2015-04-26 20:37:19 +02:00
chris f00c6018e7 Some refactoring
- I didn't like the way the context was initialized
- The DatabaseRepository got too complicated
2015-04-24 11:11:08 +02:00
chris c99d3f0db8 Forgot to add this to the documentation commit 2015-04-24 09:54:44 +02:00
chris d3499c9627 Added some address generation and handling 2015-04-24 09:53:58 +02:00
chris 096f5db1bb Documentation 2015-04-22 23:10:09 +02:00
chris 51c1ecfd41 Minor refactoring
I think having the read method in the same place as the write method might help preventing some errors
There are some people who are strictly against using static methods. I suppose I'm not experienced enough yet.
2015-04-18 16:44:00 +02:00
chris 2cd857dd36 If we compare IVs, they of course must properly implement the equals method
This fixes the bug where we would request objects again that were already stored.
2015-04-18 14:34:04 +02:00
chris ceae11b5c6 Minor DB updates
A node can support multiple streams. Unfortunately, an address can't so there might be multiple entries of the same address for different streams.
Also, store object types. I think we'll need them later, specially in search for public keys we requested.
2015-04-18 14:32:09 +02:00
chris e611e5e20a More or less properly shutdown client / network node 2015-04-17 13:01:46 +02:00
chris 8d7b9f6457 Improved POW to use all cores. Interestingly, speed up factor seems to be greater than the number of cores (but still quite slow for real word application) 2015-04-17 11:17:39 +02:00
chris 388a10fe8a Moved POW to its own port so it will be easily possible to create other (faster) implementations 2015-04-14 21:52:21 +02:00
chris 32de01bbf5 Some basics seem to work now like storing messages, addresses, inventory vectors etc. 2015-04-14 19:47:53 +02:00
chris 751ed0c637 Some more POW work - it seems to work, but is very, very, extremely slow. 2015-04-14 19:40:10 +02:00
chris daa9a9911b Added some POW code, it probably doesn't work yet
(takes very long and uses lots of battery, but I didn't get a result yet)
2015-04-11 16:16:41 +02:00
chris 9b383e3bcd Added comments and a few more object types 2015-04-08 20:54:00 +02:00
chris 35088ca033 Network code now works well enough for the server to think it successfully established a connection 2015-04-07 18:48:58 +02:00
chris 3299d8ca4a Networking code, untested 2015-04-06 10:01:03 +02:00
chris 0c4b39bdee It's now possible to send a 'version' message that will be accepted by the other node. 2015-03-31 21:06:42 +02:00
chris c2624dcd15 Added README.md 2015-03-20 14:24:12 +01:00
chris caf66ea9a5 Some basic entities and project structure 2015-03-20 14:18:29 +01:00
229 changed files with 21614 additions and 1031 deletions
+7
View File
@@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4
+162 -3
View File
@@ -1,3 +1,162 @@
_site
.sass-cache
.jekyll-metadata
# Created by https://www.gitignore.io
*.log
### Gradle ###
.gradle
build/
classes/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
### Eclipse ###
*.pydevproject
.metadata
.gradle
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# PDT-specific
.buildpath
# sbteclipse plugin
.target
# TeXlipse plugin
.texlipse
### Linux ###
*~
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
+10
View File
@@ -0,0 +1,10 @@
language: java
sudo: false # faster builds
jdk:
- oraclejdk8
before_install:
- pip install --user codecov
after_success:
- codecov
+1308
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
# Contributing
We love pull requests from everyone. Please be nice and forgive us
if we can't process your request right away.
Fork, then clone the repo:
git clone git@github.com:your-username/Jabit.git
Make sure the tests pass:
./gradlew test
Make your change. Add tests for your change. Make the tests pass:
./gradlew test
Push to your fork and [submit a pull request][pr].
[pr]: https://github.com/Dissem/Jabit/compare/
Unfortunately we can't always answer right away, so we ask you to have
some patience. Then we may suggest some changes or improvements or
alternatives.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+137
View File
@@ -0,0 +1,137 @@
Jabit
=====
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core)
[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.jabit/jabit-core/badge.svg)](http://www.javadoc.io/doc/ch.dissem.jabit/jabit-core)
[![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE)
[![Visit our IRC channel](https://img.shields.io/badge/irc-%23jabit-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#jabit)
A Java implementation for the Bitmessage protocol. To build, use command `./gradlew build`.
Please note that it still has its limitations, but the API should now be stable. Jabit uses Semantic Versioning, meaning as long as the major version doesn't change, nothing should break if you update.
Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch, notably when it comes to database updates. In other words, they may break your installation!_
#### Master
[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit)
[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/master.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144281)
[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=master)](https://codecov.io/github/Dissem/Jabit?branch=master)
#### Develop
[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=develop)](https://travis-ci.org/Dissem/Jabit?branch=develop)
[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/develop.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144279)
[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=develop)](https://codecov.io/github/Dissem/Jabit?branch=develop)
Upgrading
---------
Please be aware that Version 2.0.0 has some breaking changes, most notably in the repository implementations -- please take special care when upgrading them. If you don't implement your own repositories, you should be able to quickly find and fix any compilation errors caused by the few other breaking changes.
There is also a new network handler which comes highly recommended. If you're having any network problems, please make sure you use `NioNetworkHandler` instead of the now deprecated `DefaultNetworkHandler`.
Security
--------
There are most probably some security issues, me programming this thing all by myself. Jabit doesn't do anything against timing attacks yet, for example. Please feel free to use the library, report bugs and maybe even help out. I hope the code is easy to understand and work with.
Project Status
--------------
Basically, everything needed for a working Bitmessage client is there:
* Creating new identities (private addresses)
* Adding contacts and subscriptions
* Receiving broadcasts
* Receiving messages
* Sending messages and broadcasts
* Managing outgoing and incoming connections
* Initialise and manage a registry of Bitmessage network nodes
* An easy to use API
* A command line demo application built using the API
Setup
-----
It is recommended to define the version like this:
```Gradle
ext.jabitVersion = '2.0.0'
```
Add Jabit as Gradle dependency:
```Gradle
compile "ch.dissem.jabit:jabit-core:$jabitVersion"
```
Unless you want to implement your own, also add the following:
```Gradle
compile "ch.dissem.jabit:jabit-networking:$jabitVersion"
compile "ch.dissem.jabit:jabit-repositories:$jabitVersion"
compile "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion"
```
And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add:
```Gradle
compile "ch.dissem.jabit:jabit-wif:$jabitVersion"
```
For Android clients use `jabit-cryptography-spongy` instead of `jabit-cryptography-bouncy`.
Usage
-----
First, you'll need to create a `BitmessageContext`:
```Java
JdbcConfig jdbcConfig = new JdbcConfig();
BitmessageContext ctx = new BitmessageContext.Builder()
.addressRepo(new JdbcAddressRepository(jdbcConfig))
.inventory(new JdbcInventory(jdbcConfig))
.messageRepo(new JdbcMessageRepository(jdbcConfig))
.powRepo(new JdbcProofOfWorkRepository(jdbcConfig))
.nodeRegistry(new JdbcNodeRegistry(jdbcConfig))
.networkHandler(new NioNetworkHandler())
.cryptography(new BouncyCryptography())
.listener(System.out::println)
.build();
```
This creates a simple context using a H2 database that will be created in the user's home directory. In the listener you decide what happens when a message arrives. If you can't use lambdas, you may instead write
```Java
.listener(new BitmessageContext.Listener() {
@Override
public void receive(Plaintext plaintext) {
// TODO: Notify the user
}
})
```
Next you'll need to start the context:
```Java
ctx.startup()
```
Then you might want to create an identity
```Java
BitmessageAddress identity = ctx.createIdentity(false, Pubkey.Feature.DOES_ACK);
```
or add some contacts
```Java
BitmessageAddress contact = new BitmessageAddress("BM-2cTarrmjMdRicKZ4qQ8A13JhoR3Uq6Zh5j");
address.setAlias("Chris");
ctx.addContact(contact);
```
to which you can send some messages
```Java
ctx.send(identity, contact, "Test", "Hello Chris, this is a message.");
```
### Housekeeping
As Bitmessage stores all currently valid messages, we'll need to delete expired objects from time to time:
```Java
ctx.cleanup();
```
If the client runs all the time, it might be a good idea to do this daily or at least weekly. Otherwise, you might just want to clean up on shutdown.
Also, if some messages weren't acknowledged when it expired, they can be resent:
```Java
ctx.resendUnacknowledgedMessages();
```
This could be triggered periodically, or manually by the user. Please be aware that _if_ there is a message to resend, proof of work needs to be calculated, so to not annoy your users you might not want to trigger it on shutdown. As the client might have been offline for some time, it might as well be wise to wait until it caught up downloading new messages before resending those messages, after all they might be acknowledged by now.
There probably won't happen extremely bad things if you don't - at least not more than otherwise - but you can properly shutdown the network connection by calling
```Java
ctx.shutdown();
```
-20
View File
@@ -1,20 +0,0 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely need to edit after that.
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'jekyll serve'. If you change this file, please restart the server process.
# Site settings
title: Jabit
email: chrigu.meyer@gmail.com
description: > # this means to ignore newlines until "baseurl:"
You want to create an awesome Bitmessage app using Jabit? Then you've come
to the right place!
baseurl: "/Jabit" # the subpath of your site, e.g. /blog
url: "http://dissem.github.io" # the base hostname & protocol for your site
# twitter_username: jekyllrb
github_username: Dissem
# Build settings
markdown: kramdown
-29
View File
@@ -1,29 +0,0 @@
---
layout: post
title: "Setting up Jabit for Your Project"
date: 2016-06-22 00:01:00 +0200
categories: setup
---
Add Jabit as Gradle dependency:
{% highlight groovy %}
compile 'ch.dissem.jabit:jabit-core:1.0.0'
{% endhighlight %}
Unless you want to implement your own, also add the following:
{% highlight groovy %}
compile 'ch.dissem.jabit:jabit-networking:1.0.0'
compile 'ch.dissem.jabit:jabit-repositories:1.0.0'
compile 'ch.dissem.jabit:jabit-cryptography-bouncy:1.0.0'
{% endhighlight %}
And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add:
{% highlight groovy %}
compile 'ch.dissem.jabit:jabit-wif:1.0.0'
{% endhighlight %}
For Android clients use `jabit-cryptography-spongy` instead of `jabit-cryptography-bouncy`.
-54
View File
@@ -1,54 +0,0 @@
---
layout: post
title: "Using the Jabit API"
date: 2016-06-22 00:02:00 +0200
categories: setup
---
### Usage
First, you'll need to create a `BitmessageContext`:
{% highlight java %}
JdbcConfig jdbcConfig = new JdbcConfig();
BitmessageContext ctx = new BitmessageContext.Builder()
.addressRepo(new JdbcAddressRepository(jdbcConfig))
.inventory(new JdbcInventory(jdbcConfig))
.messageRepo(new JdbcMessageRepository(jdbcConfig))
.nodeRegistry(new MemoryNodeRegistry())
.networkHandler(new NetworkNode())
.cryptography(new BouncyCryptography())
.build();
{% endhighlight %}
This creates a simple context using a H2 database that will be created in the user's home directory. Next you'll need to
start the context and decide what happens if a message arrives:
{% highlight java %}
ctx.startup(new BitmessageContext.Listener() {
@Override
public void receive(Plaintext plaintext) {
// TODO: Notify the user
}
});
{% endhighlight %}
Then you might want to create an identity
{% highlight java %}
BitmessageAddress identity = ctx.createIdentity(false, Pubkey.Feature.DOES_ACK);
{% endhighlight %}
or add some contacts
{% highlight java %}
BitmessageAddress contact = new BitmessageAddress("BM-2cTarrmjMdRicKZ4qQ8A13JhoR3Uq6Zh5j");
address.setAlias("Chris");
ctx.addContact(contact);
{% endhighlight %}
to which you can send some messages
{% highlight java %}
ctx.send(identity, contact, "Test", "Hello Chris, this is a message.");
{% endhighlight %}
-38
View File
@@ -1,38 +0,0 @@
<footer class="site-footer">
<div class="wrapper">
<h2 class="footer-heading">{{ site.title }}</h2>
<div class="footer-col-wrapper">
<div class="footer-col footer-col-1">
<ul class="contact-list">
<li>{{ site.title }}</li>
<li><a href="mailto:{{ site.email }}">{{ site.email }}</a></li>
</ul>
</div>
<div class="footer-col footer-col-2">
<ul class="social-media-list">
{% if site.github_username %}
<li>
{% include icon-github.html username=site.github_username %}
</li>
{% endif %}
{% if site.twitter_username %}
<li>
{% include icon-twitter.html username=site.twitter_username %}
</li>
{% endif %}
</ul>
</div>
<div class="footer-col footer-col-3">
<p>{{ site.description }}</p>
</div>
</div>
</div>
</footer>
-12
View File
@@ -1,12 +0,0 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
<meta name="description" content="{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}">
<link rel="stylesheet" href="{{ "/css/main.css" | prepend: site.baseurl }}">
<link rel="canonical" href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}">
<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ "/feed.xml" | prepend: site.baseurl | prepend: site.url }}">
</head>
-28
View File
@@ -1,28 +0,0 @@
<header class="site-header">
<div class="wrapper">
<a class="site-title" href="{{ site.baseurl }}/">{{ site.title }}</a>
<nav class="site-nav">
<a href="#" class="menu-icon">
<svg viewBox="0 0 18 15">
<path fill="#424242" d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/>
<path fill="#424242" d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/>
<path fill="#424242" d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/>
</svg>
</a>
<div class="trigger">
{% assign sorted_pages = site.pages | sort: 'order' %}
{% for my_page in sorted_pages %}
{% if my_page.title %}
<a class="page-link" href="{{ my_page.url | prepend: site.baseurl }}">{{ my_page.title }}</a>
{% endif %}
{% endfor %}
</div>
</nav>
</div>
</header>
-1
View File
@@ -1 +0,0 @@
<a href="https://github.com/{{ include.username }}"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">{{ include.username }}</span></a>
-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 16 16"><path fill="#828282" d="M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z"/></svg>

Before

Width:  |  Height:  |  Size: 926 B

-1
View File
@@ -1 +0,0 @@
<a href="https://twitter.com/{{ include.username }}"><span class="icon icon--twitter">{% include icon-twitter.svg %}</span><span class="username">{{ include.username }}</span></a>
-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 16 16"><path fill="#828282" d="M15.969,3.058c-0.586,0.26-1.217,0.436-1.878,0.515c0.675-0.405,1.194-1.045,1.438-1.809c-0.632,0.375-1.332,0.647-2.076,0.793c-0.596-0.636-1.446-1.033-2.387-1.033c-1.806,0-3.27,1.464-3.27,3.27 c0,0.256,0.029,0.506,0.085,0.745C5.163,5.404,2.753,4.102,1.14,2.124C0.859,2.607,0.698,3.168,0.698,3.767 c0,1.134,0.577,2.135,1.455,2.722C1.616,6.472,1.112,6.325,0.671,6.08c0,0.014,0,0.027,0,0.041c0,1.584,1.127,2.906,2.623,3.206 C3.02,9.402,2.731,9.442,2.433,9.442c-0.211,0-0.416-0.021-0.615-0.059c0.416,1.299,1.624,2.245,3.055,2.271 c-1.119,0.877-2.529,1.4-4.061,1.4c-0.264,0-0.524-0.015-0.78-0.046c1.447,0.928,3.166,1.469,5.013,1.469 c6.015,0,9.304-4.983,9.304-9.304c0-0.142-0.003-0.283-0.009-0.423C14.976,4.29,15.531,3.714,15.969,3.058z"/></svg>

Before

Width:  |  Height:  |  Size: 787 B

-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
{% include head.html %}
<body>
{% include header.html %}
<div class="page-content">
<div class="wrapper">
{{ content }}
</div>
</div>
{% include footer.html %}
</body>
</html>
-14
View File
@@ -1,14 +0,0 @@
---
layout: default
---
<article class="post">
<header class="post-header">
<h1 class="post-title">{{ page.title }}</h1>
</header>
<div class="post-content">
{{ content }}
</div>
</article>
-15
View File
@@ -1,15 +0,0 @@
---
layout: default
---
<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
<header class="post-header">
<h1 class="post-title" itemprop="name headline">{{ page.title }}</h1>
<p class="post-meta"><time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">{{ page.date | date: "%b %-d, %Y" }}</time>{% if page.author %} • <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">{{ page.author }}</span></span>{% endif %}</p>
</header>
<div class="post-content" itemprop="articleBody">
{{ content }}
</div>
</article>
-212
View File
@@ -1,212 +0,0 @@
/**
* Reset some basic elements
*/
body, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, hr,
dl, dd, ol, ul, figure {
margin: 0;
padding: 0;
}
/**
* Basic styling
*/
body {
font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;
color: $text-color;
background-color: $background-color;
-webkit-text-size-adjust: 100%;
-webkit-font-feature-settings: "kern" 1;
-moz-font-feature-settings: "kern" 1;
-o-font-feature-settings: "kern" 1;
font-feature-settings: "kern" 1;
font-kerning: normal;
}
/**
* Set `margin-bottom` to maintain vertical rhythm
*/
h1, h2, h3, h4, h5, h6,
p, blockquote, pre,
ul, ol, dl, figure,
%vertical-rhythm {
margin-bottom: $spacing-unit / 2;
}
/**
* Images
*/
img {
max-width: 100%;
vertical-align: middle;
}
/**
* Figures
*/
figure > img {
display: block;
}
figcaption {
font-size: $small-font-size;
}
/**
* Lists
*/
ul, ol {
margin-left: $spacing-unit;
}
li {
> ul,
> ol {
margin-bottom: 0;
}
}
/**
* Headings
*/
h1, h2, h3, h4, h5, h6 {
font-weight: $base-font-weight;
}
/**
* Links
*/
a {
color: $brand-color;
text-decoration: none;
&:visited {
color: darken($brand-color, 15%);
}
&:hover {
color: $text-color;
text-decoration: underline;
}
}
/**
* Blockquotes
*/
blockquote {
color: $grey-color;
border-left: 4px solid $grey-color-light;
padding-left: $spacing-unit / 2;
font-size: 18px;
letter-spacing: -1px;
font-style: italic;
> :last-child {
margin-bottom: 0;
}
}
.info {
color: $text-color;
background-color: $blue-color-light;
border-left: 4px solid $blue-color;
}
/**
* Code formatting
*/
pre,
code {
font-size: 15px;
border: 1px solid $grey-color-light;
border-radius: 3px;
background-color: #eef;
}
code {
padding: 1px 5px;
}
pre {
padding: 8px 12px;
overflow-x: auto;
> code {
border: 0;
padding-right: 0;
padding-left: 0;
}
}
/**
* Wrapper
*/
.wrapper {
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
margin-right: auto;
margin-left: auto;
padding-right: $spacing-unit;
padding-left: $spacing-unit;
@extend %clearfix;
@include media-query($on-laptop) {
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
max-width: calc(#{$content-width} - (#{$spacing-unit}));
padding-right: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
}
/**
* Clearfix
*/
%clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
/**
* Icons
*/
.icon {
> svg {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
path {
fill: $grey-color;
}
}
}
-242
View File
@@ -1,242 +0,0 @@
/**
* Site header
*/
.site-header {
border-top: 5px solid $grey-color-dark;
border-bottom: 1px solid $grey-color-light;
min-height: 56px;
// Positioning context for the mobile navigation icon
position: relative;
}
.site-title {
font-size: 26px;
font-weight: 300;
line-height: 56px;
letter-spacing: -1px;
margin-bottom: 0;
float: left;
&,
&:visited {
color: $grey-color-dark;
}
}
.site-nav {
float: right;
line-height: 56px;
.menu-icon {
display: none;
}
.page-link {
color: $text-color;
line-height: $base-line-height;
// Gaps between nav items, but not on the last one
&:not(:last-child) {
margin-right: 20px;
}
}
@include media-query($on-palm) {
position: absolute;
top: 9px;
right: $spacing-unit / 2;
background-color: $background-color;
border: 1px solid $grey-color-light;
border-radius: 5px;
text-align: right;
.menu-icon {
display: block;
float: right;
width: 36px;
height: 26px;
line-height: 0;
padding-top: 10px;
text-align: center;
> svg {
width: 18px;
height: 15px;
path {
fill: $grey-color-dark;
}
}
}
.trigger {
clear: both;
display: none;
}
&:hover .trigger {
display: block;
padding-bottom: 5px;
}
.page-link {
display: block;
padding: 5px 10px;
&:not(:last-child) {
margin-right: 0;
}
margin-left: 20px;
}
}
}
/**
* Site footer
*/
.site-footer {
border-top: 1px solid $grey-color-light;
padding: $spacing-unit 0;
}
.footer-heading {
font-size: 18px;
margin-bottom: $spacing-unit / 2;
}
.contact-list,
.social-media-list {
list-style: none;
margin-left: 0;
}
.footer-col-wrapper {
font-size: 15px;
color: $grey-color;
margin-left: -$spacing-unit / 2;
@extend %clearfix;
}
.footer-col {
float: left;
margin-bottom: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
.footer-col-1 {
width: -webkit-calc(35% - (#{$spacing-unit} / 2));
width: calc(35% - (#{$spacing-unit} / 2));
}
.footer-col-2 {
width: -webkit-calc(20% - (#{$spacing-unit} / 2));
width: calc(20% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(45% - (#{$spacing-unit} / 2));
width: calc(45% - (#{$spacing-unit} / 2));
}
@include media-query($on-laptop) {
.footer-col-1,
.footer-col-2 {
width: -webkit-calc(50% - (#{$spacing-unit} / 2));
width: calc(50% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
@include media-query($on-palm) {
.footer-col {
float: none;
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
/**
* Page content
*/
.page-content {
padding: $spacing-unit 0;
}
.page-heading {
font-size: 20px;
}
.post-list {
margin-left: 0;
list-style: none;
> li {
margin-bottom: $spacing-unit;
}
}
.post-meta {
font-size: $small-font-size;
color: $grey-color;
}
.post-link {
display: block;
font-size: 24px;
}
/**
* Posts
*/
.post-header {
margin-bottom: $spacing-unit;
}
.post-title {
font-size: 42px;
letter-spacing: -1px;
line-height: 1;
@include media-query($on-laptop) {
font-size: 36px;
}
}
.post-content {
margin-bottom: $spacing-unit;
h2 {
font-size: 32px;
@include media-query($on-laptop) {
font-size: 28px;
}
}
h3 {
font-size: 26px;
@include media-query($on-laptop) {
font-size: 22px;
}
}
h4 {
font-size: 20px;
@include media-query($on-laptop) {
font-size: 18px;
}
}
}
-71
View File
@@ -1,71 +0,0 @@
/**
* Syntax highlighting styles
*/
.highlight {
background: #fff;
@extend %vertical-rhythm;
.highlighter-rouge & {
background: #eef;
}
.c { color: #998; font-style: italic } // Comment
.err { color: #a61717; background-color: #e3d2d2 } // Error
.k { font-weight: bold } // Keyword
.o { font-weight: bold } // Operator
.cm { color: #998; font-style: italic } // Comment.Multiline
.cp { color: #999; font-weight: bold } // Comment.Preproc
.c1 { color: #998; font-style: italic } // Comment.Single
.cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special
.gd { color: #000; background-color: #fdd } // Generic.Deleted
.gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific
.ge { font-style: italic } // Generic.Emph
.gr { color: #a00 } // Generic.Error
.gh { color: #999 } // Generic.Heading
.gi { color: #000; background-color: #dfd } // Generic.Inserted
.gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific
.go { color: #888 } // Generic.Output
.gp { color: #555 } // Generic.Prompt
.gs { font-weight: bold } // Generic.Strong
.gu { color: #aaa } // Generic.Subheading
.gt { color: #a00 } // Generic.Traceback
.kc { font-weight: bold } // Keyword.Constant
.kd { font-weight: bold } // Keyword.Declaration
.kp { font-weight: bold } // Keyword.Pseudo
.kr { font-weight: bold } // Keyword.Reserved
.kt { color: #458; font-weight: bold } // Keyword.Type
.m { color: #099 } // Literal.Number
.s { color: #d14 } // Literal.String
.na { color: #008080 } // Name.Attribute
.nb { color: #0086B3 } // Name.Builtin
.nc { color: #458; font-weight: bold } // Name.Class
.no { color: #008080 } // Name.Constant
.ni { color: #800080 } // Name.Entity
.ne { color: #900; font-weight: bold } // Name.Exception
.nf { color: #900; font-weight: bold } // Name.Function
.nn { color: #555 } // Name.Namespace
.nt { color: #000080 } // Name.Tag
.nv { color: #008080 } // Name.Variable
.ow { font-weight: bold } // Operator.Word
.w { color: #bbb } // Text.Whitespace
.mf { color: #099 } // Literal.Number.Float
.mh { color: #099 } // Literal.Number.Hex
.mi { color: #099 } // Literal.Number.Integer
.mo { color: #099 } // Literal.Number.Oct
.sb { color: #d14 } // Literal.String.Backtick
.sc { color: #d14 } // Literal.String.Char
.sd { color: #d14 } // Literal.String.Doc
.s2 { color: #d14 } // Literal.String.Double
.se { color: #d14 } // Literal.String.Escape
.sh { color: #d14 } // Literal.String.Heredoc
.si { color: #d14 } // Literal.String.Interpol
.sx { color: #d14 } // Literal.String.Other
.sr { color: #009926 } // Literal.String.Regex
.s1 { color: #d14 } // Literal.String.Single
.ss { color: #990073 } // Literal.String.Symbol
.bp { color: #999 } // Name.Builtin.Pseudo
.vc { color: #008080 } // Name.Variable.Class
.vg { color: #008080 } // Name.Variable.Global
.vi { color: #008080 } // Name.Variable.Instance
.il { color: #099 } // Literal.Number.Integer.Long
}
-12
View File
@@ -1,12 +0,0 @@
---
layout: page
title: About
permalink: /about/
order: 1000
---
Jabit is a Bitmessage library for Java. It aims to be easy to use, so you can create great Bitmessage apps.
You can find the source code for the Jabit at:
{% include icon-github.html username="Dissem" %} /
[Jabit](https://github.com/Dissem/Jabit)
-35
View File
@@ -1,35 +0,0 @@
---
layout: page
title: "API"
permalink: "/api"
order: 200
categories: api
---
> See [Jabit's JavaDoc](http://www.javadoc.io/doc/ch.dissem.jabit/jabit-core/1.0.1) for a detailed documentation of all classes and methods. Feel free to either open an issue or create a pull request if you find something to be unclear or incorrect.
{: .info}
# Context Setup
The `BitmessageContext` provides all methods a usual Bitmessage client needs. Note that all its methods should be thread safe, but you'll need to check your `BitmessageContext.Listener` for thread safety. The listener may even be called simultaneously from different threads.
To keep the library small, flexible and independent from (but compatible to) heavy-duty frameworks such as Spring, the `BitmessageContext` needs to be initialized with all its dependencies.
# Creating Identities
{% highlight java %}
BitmessageAddress identity = ctx.createIdentity(false, Pubkey.Feature.DOES_ACK);
{% endhighlight %}
If _shorter_ is enabled, Jabit searches a private key that yields a slightly shorter address. This is not optimized and may take a long time to complete.
# Importing Identities
Identities can be imported from PyBitmessages keys.dat by using the WifImporter from the library `jabit-wif`. The constructor needs a `BitmessageContext` and either a file, a string representation of a keys.dat or an input stream. The importer will throw an exception if the data can't be parsed.
Once constructed you can choose to list the contained identities, import all, a subset or a specific identity into the given `BitmessageContext`.
# Exporting Identities
Exports work very similar to imports. A `WifExporter` also needs a `BitmessageContext` to construct. You then can add either all or specific identities to the importer. Id you provide specific identities, make sure they have a private key. They will not be reloaded.
If you're done adding identities, call one of the `write` methods to save the exported identities, or `toString()` for the string representation.
# Threads
> TODO
+159
View File
@@ -0,0 +1,159 @@
buildscript {
ext.kotlin_version = '1.2.71'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
plugins {
id 'com.github.ben-manes.versions' version '0.17.0'
id "io.spring.dependency-management" version "1.0.4.RELEASE"
}
subprojects {
apply plugin: 'io.spring.dependency-management'
apply plugin: 'kotlin'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'jacoco'
apply plugin: 'gitflow-version'
apply plugin: 'com.github.ben-manes.versions'
sourceCompatibility = 1.7
targetCompatibility = 1.7
group = 'ch.dissem.jabit'
repositories {
mavenCentral()
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
jcenter()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
implementation "org.jetbrains.kotlin:kotlin-reflect"
}
test {
testLogging {
exceptionFormat = 'full'
}
}
task javadocJar(type: Jar) {
classifier = 'javadoc'
from javadoc
}
task sourcesJar(type: Jar) {
classifier = 'sources'
from sourceSets.main.allSource
}
compileKotlin {
kotlinOptions.jvmTarget = "1.6"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.6"
}
artifacts {
archives javadocJar, sourcesJar
}
jar {
manifest {
attributes 'Implementation-Title': "Jabit ${project.name.capitalize()}",
'Implementation-Version': version
}
baseName "jabit-${project.name}"
}
signing {
required { isRelease && project.getProperties().get("signing.keyId")?.length() > 0 }
sign configurations.archives
}
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name 'Jabit'
packaging 'jar'
url 'https://github.com/Dissem/Jabit'
scm {
connection 'scm:git:https://github.com/Dissem/Jabit.git'
developerConnection 'scm:git:git@github.com:Dissem/Jabit.git'
url 'https://github.com/Dissem/Jabit.git'
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
name 'Christian Basler'
email 'chrigu.meyer@gmail.com'
}
}
}
}
}
}
jacocoTestReport {
reports {
xml.enabled = true
html.enabled = true
}
}
check.dependsOn jacocoTestReport
dependencyManagement {
dependencies {
dependencySet(group: 'org.jetbrains.kotlin', version: "$kotlin_version") {
entry 'kotlin-stdlib-jdk7'
entry 'kotlin-reflect'
}
dependencySet(group: 'org.slf4j', version: '1.7.25') {
entry 'slf4j-api'
entry 'slf4j-simple'
}
dependency 'ch.dissem.msgpack:msgpack:2.0.1'
dependency 'org.bouncycastle:bcprov-jdk15on:1.60'
dependency 'com.madgag.spongycastle:prov:1.58.0.0'
dependency 'org.apache.commons:commons-text:1.5'
dependency 'org.flywaydb:flyway-core:5.2.0'
dependency 'com.beust:klaxon:3.0.8'
dependency 'args4j:args4j:2.33'
dependency 'org.ini4j:ini4j:0.5.4'
dependency 'com.h2database:h2:1.4.197'
dependency 'org.hamcrest:java-hamcrest:2.0.0.0'
dependency 'com.nhaarman:mockito-kotlin:1.6.0'
dependency 'org.junit.jupiter:junit-jupiter-api:5.3.1'
dependency 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
}
}
}
@@ -0,0 +1,57 @@
package ch.dissem.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Sets the version as follows:
* <ul>
* <li>If the branch is 'master', the version is set to the latest tag (which is expected to be set by Git flow)</li>
* <li>Otherwise, the version is set to the branch name, with '-SNAPSHOT' appended</li>
* </ul>
*/
class GitFlowVersion implements Plugin<Project> {
def getBranch(Project project) {
def stdout = new ByteArrayOutputStream()
project.exec {
commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
}
def getTag(Project project) {
def stdout = new ByteArrayOutputStream()
project.exec {
commandLine 'git', 'describe', '--abbrev=0'
standardOutput = stdout
}
return stdout.toString().trim()
}
def isRelease(Project project) {
return "master" == getBranch(project);
}
def getVersion(Project project) {
if (project.ext.isRelease) {
return getTag(project)
} else {
def branch = getBranch(project)
if ("develop" == branch) {
return "development-SNAPSHOT"
}
return branch.replaceAll("/", "-") + "-SNAPSHOT"
}
}
@Override
void apply(Project project) {
project.ext.isRelease = isRelease(project)
project.version = getVersion(project)
project.task('version') {
doLast { println "Version deduced from git: '${project.version}'" }
}
}
}
@@ -0,0 +1 @@
implementation-class=ch.dissem.gradle.GitFlowVersion
+51
View File
@@ -0,0 +1,51 @@
uploadArchives {
repositories {
mavenDeployer {
pom.project {
name 'Jabit Core'
artifactId = 'jabit-core'
description 'A Java implementation of the Bitmessage protocol. This is the core part. You\'ll either need the networking and repositories modules, too, or implement your own.'
}
}
}
}
configurations {
testArtifacts.extendsFrom testRuntime
}
task testJar(type: Jar) {
classifier = 'test'
from sourceSets.test.output
}
artifacts {
testArtifacts testJar
}
dependencies {
compile 'org.slf4j:slf4j-api'
compile 'ch.dissem.msgpack:msgpack'
testCompile 'com.nhaarman:mockito-kotlin'
testCompile 'org.junit.jupiter:junit-jupiter-api'
testRuntime 'org.junit.jupiter:junit-jupiter-engine'
testCompile project(':cryptography-bc')
}
def generatedResources = "${project.buildDir}/generated-resources/main"
sourceSets {
main {
output.dir(generatedResources, builtBy: 'generateVersionInfo')
}
}
task('generateVersionInfo') {
doLast {
def dir = new File(generatedResources)
if (!dir.exists()) {
dir.mkdirs()
}
def file = new File(generatedResources, "version")
file.write(project.version.toString())
}
}
@@ -0,0 +1,149 @@
/*
* 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 helper class for working with byte arrays interpreted as unsigned big endian integers.
* This is one part due to the fact that Java doesn't support unsigned numbers, and another
* part so we don't have to convert between byte arrays and numbers in time critical
* situations.
* <p>
* Note: This class can't yet be ported to Kotlin, as with Kotlin byte + byte = int, which
* would be rather inefficient in our case.
* </p>
*/
public class Bytes {
public static final byte BYTE_0x80 = (byte) 0x80;
public static void inc(byte[] nonce) {
for (int i = nonce.length - 1; i >= 0; i--) {
nonce[i]++;
if (nonce[i] != 0) break;
}
}
/**
* Increases nonce by value, which is used as an unsigned byte value.
*
* @param nonce an unsigned number
* @param value to be added to nonce
*/
public static void inc(byte[] nonce, byte value) {
int i = nonce.length - 1;
nonce[i] += value;
if (value > 0 && (nonce[i] < 0 || nonce[i] >= value))
return;
if (value < 0 && (nonce[i] < 0 && nonce[i] >= value))
return;
for (i = i - 1; i >= 0; i--) {
nonce[i]++;
if (nonce[i] != 0) break;
}
}
/**
* @return true if a &lt; b.
*/
public static boolean lt(byte[] a, byte[] b) {
byte[] max = (a.length > b.length ? a : b);
byte[] min = (max == a ? b : a);
int diff = max.length - min.length;
for (int i = 0; i < max.length - min.length; i++) {
if (max[i] != 0) return a != max;
}
for (int i = diff; i < max.length; i++) {
if (max[i] != min[i - diff]) {
return lt(max[i], min[i - diff]) == (a == max);
}
}
return false;
}
/**
* @return true if a &lt; b, where the first [size] bytes are used as the numbers to check.
*/
public static boolean lt(byte[] a, byte[] b, int size) {
for (int i = 0; i < size; i++) {
if (a[i] != b[i]) {
return lt(a[i], b[i]);
}
}
return false;
}
private static boolean lt(byte a, byte b) {
return (a ^ BYTE_0x80) < (b ^ BYTE_0x80);
}
/**
* @return a new byte array of length, left-padded with '0'.
*/
public static byte[] expand(byte[] source, int size) {
byte[] result = new byte[size];
System.arraycopy(source, 0, result, size - source.length, source.length);
return result;
}
/**
* @return a new byte array containing the first <em>size</em> bytes of the given array.
*/
public static byte[] truncate(byte[] source, int size) {
byte[] result = new byte[size];
System.arraycopy(source, 0, result, 0, size);
return result;
}
/**
* @return the byte array that hex represents. This is meant for test use and should be rewritten if used in
* production code.
*/
public static byte[] fromHex(String hex) {
if (hex.length() % 2 != 0) throw new IllegalArgumentException("expected even number of characters");
byte[] result = new byte[hex.length() / 2];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (hexValue(hex.charAt(i * 2)) * 16);
result[i] += hexValue(hex.charAt(i * 2 + 1));
}
return result;
}
private static int hexValue(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return 10 + c - 'a';
}
if (c >= 'A' && c <= 'F') {
return 10 + c - 'A';
}
throw new IllegalArgumentException("'" + c + "' is not a valid hex value");
}
/**
* @return the number of leading '0' of a byte array.
*/
public static int numberOfLeadingZeros(byte[] bytes) {
int i;
for (i = 0; i < bytes.length; i++) {
if (bytes[i] != 0) return i;
}
return i;
}
}
@@ -0,0 +1,465 @@
/*
* 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.BitmessageContext.Companion.version
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.MINUTE
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.util.concurrent.CancellationException
import java.util.concurrent.ExecutionException
import kotlin.properties.Delegates
/**
*
* Use this class if you want to create a Bitmessage client.
* You'll need the Builder to create a BitmessageContext, and set the following properties:
*
* * addressRepo
* * inventory
* * nodeRegistry
* * networkHandler
* * messageRepo
* * streams
*
*
* The default implementations in the different module builds can be used.
*
* The port defaults to 8444 (the default Bitmessage port)
*/
class BitmessageContext private constructor(builder: BitmessageContext.Builder) {
/**
* 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 labels: LabelRepository
@JvmName("labels") 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 (internals.preferences.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 =
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()
internals.nodeRegistry.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()
}
fun isRunning() = internals.networkHandler.isRunning
fun addContact(contact: BitmessageAddress) {
internals.addressRepository.save(contact)
if (contact.pubkey == null) {
// If it already existed, the saved contact might have the public key
if (internals.addressRepository.getAddress(contact.address)!!.pubkey == null) {
internals.requestPubkey(contact)
}
}
}
fun addSubscribtion(address: BitmessageAddress) {
address.isSubscribed = true
internals.addressRepository.save(address)
tryToFindBroadcastsForAddress(address)
}
private fun tryToFindBroadcastsForAddress(address: BitmessageAddress) {
for (objectMessage in internals.inventory.getObjects(address.stream, Broadcast.getVersion(address), ObjectType.BROADCAST)) {
try {
val broadcast = objectMessage.payload as Broadcast
broadcast.decrypt(address)
// This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with
// other subscriptions and the interface stays as simple as possible.
internals.networkListener.receive(objectMessage)
} catch (ignore: DecryptionFailedException) {
} catch (e: Exception) {
LOG.debug(e.message, e)
}
}
}
fun status(): Property {
return Property("status",
Property("user agent", internals.preferences.userAgent),
internals.networkHandler.getNetworkStatus(),
Property("unacknowledged", internals.messageRepository.findMessagesToResend().size)
)
}
interface Listener {
fun receive(plaintext: Plaintext)
/**
* A message listener that needs a [BitmessageContext], i.e. for implementing some sort of chat bot.
*/
interface WithContext : Listener {
fun setContext(ctx: BitmessageContext)
}
}
/**
* Kotlin users: you might want to use [BitmessageContext.build] instead.
*/
class Builder {
var inventory by Delegates.notNull<Inventory>()
var nodeRegistry by Delegates.notNull<NodeRegistry>()
var networkHandler by Delegates.notNull<NetworkHandler>()
var addressRepo by Delegates.notNull<AddressRepository>()
var labelRepo by Delegates.notNull<LabelRepository>()
var messageRepo by Delegates.notNull<MessageRepository>()
var proofOfWorkRepo by Delegates.notNull<ProofOfWorkRepository>()
var proofOfWorkEngine: ProofOfWorkEngine? = null
var cryptography by Delegates.notNull<Cryptography>()
var customCommandHandler: CustomCommandHandler? = null
var labeler: Labeler? = null
var listener by Delegates.notNull<Listener>()
val preferences = Preferences()
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 labelRepo(labelRepo: LabelRepository): Builder {
this.labelRepo = labelRepo
return this
}
fun messageRepo(messageRepo: MessageRepository): Builder {
this.messageRepo = messageRepo
return this
}
fun powRepo(proofOfWorkRepository: ProofOfWorkRepository): Builder {
this.proofOfWorkRepo = 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
}
@JvmSynthetic
fun listener(listener: (Plaintext) -> Unit): Builder {
this.listener = object : Listener {
override fun receive(plaintext: Plaintext) {
listener.invoke(plaintext)
}
}
return this
}
fun build() = BitmessageContext(this)
}
init {
this.labeler = builder.labeler ?: DefaultLabeler()
this.internals = InternalContext(
builder.cryptography,
builder.inventory,
builder.nodeRegistry,
builder.networkHandler,
builder.addressRepo,
builder.labelRepo,
builder.messageRepo,
builder.proofOfWorkRepo,
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,
labeler,
builder.preferences
)
this.addresses = builder.addressRepo
this.labels = builder.labelRepo
this.messages = builder.messageRepo
(builder.listener as? Listener.WithContext)?.setContext(this)
internals.proofOfWorkService.doMissingProofOfWork(builder.preferences.doMissingProofOfWorkDelayInSeconds * 1000L)
}
companion object {
@JvmField
val CURRENT_VERSION = 3
private val LOG = LoggerFactory.getLogger(BitmessageContext::class.java)
val version: String by lazy {
BitmessageContext::class.java.getResource("/version")?.readText() ?: "local build"
}
@JvmStatic get
@JvmSynthetic
inline fun build(block: Builder.() -> Unit): BitmessageContext {
val builder = Builder()
block(builder)
return builder.build()
}
}
}
class Preferences {
var port = 8444
/**
* Defaults to "/Jabit:<version>/", and whatever you set will be inserted into "/<your user agent>/Jabit:<version>/"
*/
var userAgent = "/Jabit:$version/"
set(value) {
field = "/$value/Jabit:$version/"
}
/**
* Time to live for any connection
*/
var connectionTTL = 30 * MINUTE
/**
* Maximum number of connections. Values below 8 would probably result in erratic behaviour, so you shouldn't do that.
*/
var connectionLimit = 150
/**
* By default a client will send the public key when an identity is being created. On weaker devices
* this behaviour might not be desirable.
*/
var sendPubkeyOnIdentityCreation = true
/**
* Delay in seconds before outstandinng proof of work is calculated.
*/
var doMissingProofOfWorkDelayInSeconds = 30
}
@@ -0,0 +1,180 @@
/*
* 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.AlreadyStoredException
import ch.dissem.bitmessage.ports.Labeler
import ch.dissem.bitmessage.ports.NetworkHandler
import ch.dissem.bitmessage.utils.Strings.hex
import org.slf4j.LoggerFactory
import java.util.*
open class DefaultMessageListener(
private val labeler: Labeler,
private val listener: BitmessageContext.Listener
) : NetworkHandler.MessageListener, InternalContext.ContextHolder {
private lateinit var ctx: InternalContext
override fun setContext(context: InternalContext) {
ctx = context
}
override fun receive(objectMessage: ObjectMessage) {
val payload = objectMessage.payload
when (payload.type) {
ObjectType.GET_PUBKEY -> {
receive(objectMessage, payload as GetPubkey)
}
ObjectType.PUBKEY -> {
receive(payload as Pubkey)
}
ObjectType.MSG -> {
receive(objectMessage, payload as Msg)
}
ObjectType.BROADCAST -> {
receive(objectMessage, payload as Broadcast)
}
null -> {
if (payload is GenericPayload) {
receive(payload)
}
}
}
}
protected fun receive(objectMessage: ObjectMessage, getPubkey: GetPubkey) {
val identity = ctx.addressRepository.findIdentity(getPubkey.ripeTag)
if (identity?.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
try {
ctx.messageRepository.save(msg)
// We might need the ID here, so we need to add the labels and save it again
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")
}
} catch (e: AlreadyStoredException) {
LOG.trace("Message was already received before.", e)
}
}
companion object {
private val LOG = LoggerFactory.getLogger(DefaultMessageListener::class.java)
}
}
@@ -0,0 +1,225 @@
/*
* 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: Inventory,
val nodeRegistry: NodeRegistry,
val networkHandler: NetworkHandler,
val addressRepository: AddressRepository,
val labelRepository: LabelRepository,
val messageRepository: MessageRepository,
val proofOfWorkRepository: ProofOfWorkRepository,
val proofOfWorkEngine: ProofOfWorkEngine,
val customCommandHandler: CustomCommandHandler,
listener: BitmessageContext.Listener,
val labeler: Labeler,
val preferences: Preferences
) {
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
}
}
@@ -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
}
@@ -0,0 +1,52 @@
/*
* 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 writer(): StreamableWriter = Writer(this)
private class Writer(
private val item: Addr
) : StreamableWriter {
override fun write(out: OutputStream) {
Encode.varInt(item.addresses.size, out)
for (address in item.addresses) {
address.writer().write(out)
}
}
override fun write(buffer: ByteBuffer) {
Encode.varInt(item.addresses.size, buffer)
for (address in item.addresses) {
address.writer().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 input = ByteArrayInputStream(bytes)
val counter = AccessCounter()
this.version = varInt(input, counter)
this.stream = varInt(input, counter)
this.ripe = Bytes.expand(bytes(input, bytes.size - counter.length() - 4), 20)
// test checksum
var checksum = cryptography().doubleSha512(bytes, bytes.size - 4)
val expectedChecksum = bytes(input, 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,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.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 = COMMAND_ERROR == customCommand
fun getData(): ByteArray {
return data ?: {
val out = ByteArrayOutputStream()
writer().write(out)
out.toByteArray()
}.invoke()
}
override fun writer(): StreamableWriter = Writer(this)
protected open class Writer(
private val item: CustomMessage
) : StreamableWriter {
override fun write(out: OutputStream) {
if (item.data != null) {
Encode.varString(item.customCommand, out)
out.write(item.data)
} else {
throw ApplicationException("Tried to write custom message without data. "
+ "Programmer: did you forget to override #write()?")
}
}
override fun write(buffer: ByteBuffer) {
if (item.data != null) {
Encode.varString(item.customCommand, buffer)
buffer.put(item.data)
} else {
throw ApplicationException("Tried to write custom message without data. "
+ "Programmer: did you forget to override #write()?")
}
}
}
companion object {
val COMMAND_ERROR = "ERROR"
@JvmStatic
fun read(input: InputStream, length: Int): CustomMessage {
val counter = AccessCounter()
return CustomMessage(varString(input, counter), bytes(input, length - counter.length()))
}
@JvmStatic
fun error(message: String) = CustomMessage(COMMAND_ERROR, message.toByteArray(charset("UTF-8")))
}
}
@@ -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
}
@@ -0,0 +1,38 @@
/*
* 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
/**
* 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 writer(): StreamableWriter = Writer(this)
private class Writer(
item: GetData
) : InventoryWriter(item.inventory)
companion object {
@JvmField
val MAX_INVENTORY_SIZE = 50000
}
}
@@ -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
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
/**
* 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 writer(): StreamableWriter = Writer(this)
private class Writer(
item: Inv
) : InventoryWriter(item.inventory)
}
@@ -0,0 +1,40 @@
/*
* Copyright 2018 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF 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
internal open class InventoryWriter(private val inventory: List<InventoryVector>) : StreamableWriter {
override fun write(out: OutputStream) {
Encode.varInt(inventory.size, out)
for (iv in inventory) {
iv.writer().write(out)
}
}
override fun write(buffer: ByteBuffer) {
Encode.varInt(inventory.size, buffer)
for (iv in inventory) {
iv.writer().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,132 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.entity
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Singleton.cryptography
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 {
override fun writer(): Writer = Writer(this)
class Writer internal constructor(
private val item: NetworkMessage
) : StreamableWriter {
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 = item.payload.command.name.toLowerCase()
out.write(command.toByteArray(charset("ASCII")))
for (i in command.length..11) {
out.write(0x0)
}
val payloadBytes = Encode.bytes(item.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 = item.payload.command.name.toLowerCase()
out.put(command.toByteArray(charset("ASCII")))
for (i in command.length..11) {
out.put(0.toByte())
}
val payloadBytes = Encode.bytes(item.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
}
/**
* 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])
}
}
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,227 @@
/*
* 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 = payload.type?.number ?: throw IllegalArgumentException("payload must have type defined"),
/**
* The object's version
*/
val version: Long = payload.version,
val stream: Long = payload.stream
) : MessagePayload {
override val command: MessagePayload.Command = MessagePayload.Command.OBJECT
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()
writer.writeHeaderWithoutNonce(out)
payload.writer().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)
}
val payloadBytesWithoutNonce: ByteArray by lazy {
val out = ByteArrayOutputStream()
writer.writeHeaderWithoutNonce(out)
payload.writer().write(out)
out.toByteArray()
}
private val writer = Writer(this)
override fun writer(): StreamableWriter = writer
private class Writer(
private val item: ObjectMessage
) : StreamableWriter {
override fun write(out: OutputStream) {
out.write(item.nonce ?: ByteArray(8))
out.write(item.payloadBytesWithoutNonce)
}
override fun write(buffer: ByteBuffer) {
buffer.put(item.nonce ?: ByteArray(8))
buffer.put(item.payloadBytesWithoutNonce)
}
internal fun writeHeaderWithoutNonce(out: OutputStream) {
Encode.int64(item.expiresTime, out)
Encode.int32(item.type, out)
Encode.varInt(item.version, out)
Encode.varInt(item.stream, out)
}
}
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
}
}
@@ -0,0 +1,832 @@
/*
* 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
import kotlin.collections.LinkedHashSet
private fun message(encoding: Plaintext.Encoding, subject: String, body: String): ByteArray = when (encoding) {
SIMPLE -> "Subject:$subject\nBody:$body".toByteArray()
EXTENDED -> Message.Builder().subject(subject).body(body).build().zip()
TRIVIAL -> (subject + body).toByteArray()
IGNORE -> ByteArray(0)
}
private fun ackData(type: Plaintext.Type, ackData: ByteArray?): ByteArray? {
if (ackData != null) {
return ackData
} else if (type == MSG) {
return cryptography().randomBytes(Msg.ACK_LENGTH)
} else {
return null
}
}
/**
* A plaintext message before encryption or after decryption.
*/
class Plaintext private constructor(
val type: Type,
val from: BitmessageAddress,
to: BitmessageAddress?,
val encodingCode: Long,
val message: ByteArray,
val ackData: ByteArray?,
ackMessage: Lazy<ObjectMessage?> = lazy { Factory.createAck(from, ackData, ttl) },
var 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
}
this.to?.let {
if (it.version != 0L)
throw IllegalStateException("Correct address already set")
if (!Arrays.equals(it.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(
// 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,
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 = LinkedHashSet(builder.labels),
status = builder.status ?: Status.RECEIVED
) {
id = builder.id
}
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()
return when (encodingCode) {
EXTENDED.code -> if (Message.TYPE == extendedData?.type) {
(extendedData!!.content as? Message)?.subject
} else {
null
}
SIMPLE.code -> firstLine.substring("Subject:".length).trim { it <= ' ' }
else -> {
if (firstLine.length > 50) {
firstLine.substring(0, 50).trim { it <= ' ' } + "..."
} else {
firstLine
}
}
}
}
val text: String?
get() {
if (encodingCode == EXTENDED.code) {
return if (Message.TYPE == extendedData?.type) {
(extendedData?.content as Message?)?.body
} else {
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()
return if (Message.TYPE == extendedData.type) {
(extendedData.content as Message).parents
} else {
emptyList()
}
}
val files: List<Attachment>
get() {
val extendedData = extendedData ?: return emptyList()
return if (Message.TYPE == extendedData.type) {
(extendedData.content as Message).files
} else {
emptyList()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Plaintext) return false
return encoding == other.encoding &&
from.address == other.from.address &&
Arrays.equals(message, other.message) &&
ackMessage == other.ackMessage &&
Arrays.equals(to?.ripe, other.to?.ripe) &&
Arrays.equals(signature, other.signature) &&
status == other.status &&
sent == other.sent &&
received == other.received &&
labels == other.labels
}
override fun hashCode(): Int {
return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels)
}
fun addLabels(vararg labels: Label) {
Collections.addAll(this.labels, *labels)
}
fun addLabels(labels: Collection<Label>?) {
if (labels != null) {
this.labels.addAll(labels)
}
}
fun removeLabel(type: Label.Type) {
labels.removeAll { it.type == type }
}
fun isUnread(): Boolean {
return labels.any { it.type == Label.Type.UNREAD }
}
override fun toString(): String {
val subject = subject
if (subject?.isNotEmpty() == true) {
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,
PUBKEY_REQUESTED,
DOING_PROOF_OF_WORK,
SENT,
SENT_ACKNOWLEDGED,
RECEIVED
}
enum class Type {
MSG, BROADCAST
}
fun writer(includeSignature: Boolean): StreamableWriter = Writer(this, includeSignature)
override fun writer(): StreamableWriter = Writer(this)
private class Writer(
private val item: Plaintext,
private val includeSignature: Boolean = true
) : StreamableWriter {
override fun write(out: OutputStream) {
Encode.varInt(item.from.version, out)
Encode.varInt(item.from.stream, out)
item.from.pubkey?.apply {
Encode.int32(behaviorBitfield, out)
out.write(signingKey, 1, 64)
out.write(encryptionKey, 1, 64)
if (item.from.version >= 3) {
Encode.varInt(nonceTrialsPerByte, out)
Encode.varInt(extraBytes, out)
}
} ?: {
Encode.int32(0, out)
val empty = ByteArray(64)
out.write(empty)
out.write(empty)
if (item.from.version >= 3) {
Encode.varInt(0, out)
Encode.varInt(0, out)
}
}.invoke()
if (item.type == MSG) {
// A draft without recipient is allowed, therefore this workaround.
item.to?.let { out.write(it.ripe) } ?: if (item.status == Status.DRAFT) {
out.write(ByteArray(20))
} else {
throw IllegalStateException("No recipient set for message")
}
}
Encode.varInt(item.encodingCode, out)
Encode.varInt(item.message.size, out)
out.write(item.message)
if (item.type == MSG) {
if (item.to?.has(Feature.DOES_ACK) == true) {
val ack = ByteArrayOutputStream()
item.ackMessage?.writer()?.write(ack)
Encode.varBytes(ack.toByteArray(), out)
} else {
Encode.varInt(0, out)
}
}
if (includeSignature) {
val sig = item.signature
if (sig == null) {
Encode.varInt(0, out)
} else {
Encode.varBytes(sig, out)
}
}
}
override fun write(buffer: ByteBuffer) {
Encode.varInt(item.from.version, buffer)
Encode.varInt(item.from.stream, buffer)
if (item.from.pubkey == null) {
Encode.int32(0, buffer)
val empty = ByteArray(64)
buffer.put(empty)
buffer.put(empty)
if (item.from.version >= 3) {
Encode.varInt(0, buffer)
Encode.varInt(0, buffer)
}
} else {
Encode.int32(item.from.pubkey!!.behaviorBitfield, buffer)
buffer.put(item.from.pubkey!!.signingKey, 1, 64)
buffer.put(item.from.pubkey!!.encryptionKey, 1, 64)
if (item.from.version >= 3) {
Encode.varInt(item.from.pubkey!!.nonceTrialsPerByte, buffer)
Encode.varInt(item.from.pubkey!!.extraBytes, buffer)
}
}
if (item.type == MSG) {
// A draft without recipient is allowed, therefore this workaround.
item.to?.let { buffer.put(it.ripe) } ?: if (item.status == Status.DRAFT) {
buffer.put(ByteArray(20))
} else {
throw IllegalStateException("No recipient set for message")
}
}
Encode.varInt(item.encodingCode, buffer)
Encode.varBytes(item.message, buffer)
if (item.type == MSG) {
if (item.to!!.has(Feature.DOES_ACK) && item.ackMessage != null) {
Encode.varBytes(Encode.bytes(item.ackMessage!!), buffer)
} else {
Encode.varInt(0, buffer)
}
}
if (includeSignature) {
val sig = item.signature
if (sig == null) {
Encode.varInt(0, buffer)
} else {
Encode.varBytes(sig, buffer)
}
}
}
}
class Builder(internal val type: Type) {
var id: Any? = null
var inventoryVector: InventoryVector? = null
var from: BitmessageAddress? = null
var to: BitmessageAddress? = null
set(value) {
if (value != null) {
if (type != MSG && to != null)
throw IllegalArgumentException("recipient address only allowed for msg")
field = value
}
}
var addressVersion: Long = 0
var stream: Long = 0
var behaviorBitfield: Int = 0
var publicSigningKey: ByteArray? = null
var publicEncryptionKey: ByteArray? = null
var nonceTrialsPerByte: Long = 0
var extraBytes: Long = 0
var destinationRipe: ByteArray? = null
set(value) {
if (type != MSG && value != null) throw IllegalArgumentException("ripe only allowed for msg")
field = value
}
var preventAck: Boolean = false
var encoding: Long = 0
var message = ByteArray(0)
var ackData: ByteArray? = null
var ackMessage: ByteArray? = null
var signature: ByteArray? = null
var sent: Long? = null
var received: Long? = null
var status: Status? = null
var labels: Collection<Label> = emptySet()
var ttl: Long = 0
var retries: Int = 0
var nextTry: Long? = null
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 {
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 {
this.destinationRipe = ripe
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
}
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 = 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
}
internal fun prepare(): Builder {
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 (preventAck) {
ackData = null
ackMessage = null
} else if (type == MSG && ackMessage == null && ackData == null && to?.has(Feature.DOES_ACK) == true) {
ackData = cryptography().randomBytes(Msg.ACK_LENGTH)
}
if (ttl <= 0) {
ttl = TTL.msg
}
return this
}
@JvmSynthetic
inline fun build(block: Builder.() -> Unit): Plaintext {
block(this)
return build()
}
fun build(): Plaintext {
return Plaintext(this)
}
}
companion object {
@JvmStatic
fun read(type: Type, input: InputStream): Plaintext {
return readWithoutSignature(type, input)
.signature(Decode.varBytes(input))
.received(UnixTime.now)
.build()
}
@JvmStatic
fun readWithoutSignature(type: Type, input: InputStream): Plaintext.Builder {
val version = Decode.varInt(input)
return Builder(type)
.addressVersion(version)
.stream(Decode.varInt(input))
.behaviorBitfield(Decode.int32(input))
.publicSigningKey(Decode.bytes(input, 64))
.publicEncryptionKey(Decode.bytes(input, 64))
.nonceTrialsPerByte(if (version >= 3) Decode.varInt(input) else 0)
.extraBytes(if (version >= 3) Decode.varInt(input) else 0)
.destinationRipe(if (type == MSG) Decode.bytes(input, 20).let {
if (it.any { x -> x != 0.toByte() }) it else null
} else null)
.encoding(Decode.varInt(input))
.message(Decode.varBytes(input))
.ackMessage(if (type == MSG) Decode.varBytes(input) else null)
}
@JvmSynthetic
inline fun build(type: Type, block: Builder.() -> Unit): Plaintext {
val builder = Builder(type)
block(builder)
return builder.build()
}
}
}
data class Conversation(val id: UUID, val subject: String, val messages: List<Plaintext>) : Serializable {
val participants = messages
.map { it.from }
.filter { it.privateKey == null || it.isChan }
.distinct()
val extract: String by lazy {
messages.firstOrNull { m -> m.labels.any { l -> l.type==Label.Type.UNREAD } }?.text
?: messages.lastOrNull()?.text
?: ""
}
fun hasUnread() = messages.any { it.isUnread() }
}
@@ -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,50 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.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 writer(): StreamableWriter
}
interface SignedStreamable : Streamable {
override fun writer(): SignedStreamableWriter
}
interface EncryptedStreamable : SignedStreamable {
override fun writer(): EncryptedStreamableWriter
}
interface StreamableWriter: Serializable {
fun write(out: OutputStream)
fun write(buffer: ByteBuffer)
}
interface SignedStreamableWriter : StreamableWriter {
fun writeBytesToSign(out: OutputStream)
}
interface EncryptedStreamableWriter : SignedStreamableWriter {
fun writeUnencrypted(out: OutputStream)
fun writeUnencrypted(buffer: ByteBuffer)
}
@@ -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
// 'verack' doesn't have any payload, so there is nothing to write
override fun writer(): StreamableWriter = EmptyWriter
internal object EmptyWriter : StreamableWriter {
override fun write(out: OutputStream) = Unit
override fun write(buffer: ByteBuffer) = Unit
}
companion object {
private val serialVersionUID = -4302074845199181687L
}
}
@@ -0,0 +1,206 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.entity
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.UnixTime
import java.io.OutputStream
import java.nio.ByteBuffer
/**
* The 'version' command advertises this node's latest supported protocol version upon initiation.
*/
class Version constructor(
/**
* Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's
* version is lower but continue with the connection if it is higher.
*/
val version: Int = BitmessageContext.CURRENT_VERSION,
/**
* bitfield of features to be enabled for this connection
*/
val services: Long = Version.Service.getServiceFlag(Version.Service.NODE_NETWORK),
/**
* standard UNIX timestamp in seconds
*/
val timestamp: Long = UnixTime.now,
/**
* The network address of the node receiving this message (not including the time or stream number)
*/
val addrRecv: NetworkAddress,
/**
* The network address of the node emitting this message (not including the time or stream number and the ip itself
* is ignored by the receiver)
*/
val addrFrom: NetworkAddress,
/**
* Random nonce used to detect connections to self.
*/
val nonce: Long,
/**
* User Agent (0x00 if string is 0 bytes long). Sending nodes must not include a user_agent longer than 5000 bytes.
*/
val userAgent: String,
/**
* The stream numbers that the emitting node is interested in. Sending nodes must not include more than 160000
* stream numbers.
*/
val streams: LongArray = longArrayOf(1)
) : MessagePayload {
fun provides(service: Service?) = service?.isEnabled(services) == true
override val command: MessagePayload.Command = MessagePayload.Command.VERSION
override fun writer(): StreamableWriter = Writer(this)
private class Writer(
private val item: Version
) : StreamableWriter {
override fun write(out: OutputStream) {
Encode.int32(item.version, out)
Encode.int64(item.services, out)
Encode.int64(item.timestamp, out)
item.addrRecv.writer(true).write(out)
item.addrFrom.writer(true).write(out)
Encode.int64(item.nonce, out)
Encode.varString(item.userAgent, out)
Encode.varIntList(item.streams, out)
}
override fun write(buffer: ByteBuffer) {
Encode.int32(item.version, buffer)
Encode.int64(item.services, buffer)
Encode.int64(item.timestamp, buffer)
item.addrRecv.writer(true).write(buffer)
item.addrFrom.writer(true).write(buffer)
Encode.int64(item.nonce, buffer)
Encode.varString(item.userAgent, buffer)
Encode.varIntList(item.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) = (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,83 @@
/*
* 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,219 @@
/*
* 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.StreamableWriter
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()
writer.writeWithoutMAC(macData)
return cryptography().mac(key_m, macData.toByteArray())
}
private val writer = Writer(this)
override fun writer(): StreamableWriter = writer
private class Writer(
private val item: CryptoBox
) : StreamableWriter {
override fun write(out: OutputStream) {
writeWithoutMAC(out)
out.write(item.mac)
}
internal fun writeWithoutMAC(out: OutputStream) {
out.write(item.initializationVector)
Encode.int16(item.curveType, out)
writeCoordinateComponent(out, Points.getX(item.R))
writeCoordinateComponent(out, Points.getY(item.R))
out.write(item.encrypted)
}
override fun write(buffer: ByteBuffer) {
buffer.put(item.initializationVector)
Encode.int16(item.curveType, buffer)
writeCoordinateComponent(buffer, Points.getX(item.R))
writeCoordinateComponent(buffer, Points.getY(item.R))
buffer.put(item.encrypted)
buffer.put(item.mac)
}
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)
}
}
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(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,71 @@
/*
* 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.SignedStreamableWriter
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 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
}
override fun writer(): SignedStreamableWriter = Writer(this)
private class Writer(
private val item: GenericPayload
) : SignedStreamableWriter {
override fun write(out: OutputStream) {
out.write(item.data)
}
override fun write(buffer: ByteBuffer) {
buffer.put(item.data)
}
override fun writeBytesToSign(out: OutputStream) = Unit // nothing to do
}
companion object {
@JvmStatic
fun read(version: Long, stream: Long, input: InputStream, length: Int) =
GenericPayload(version, stream, Decode.bytes(input, length))
}
}
@@ -0,0 +1,75 @@
/*
* 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.SignedStreamableWriter
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 writer(): SignedStreamableWriter = Writer(this)
private class Writer(
private val item: GetPubkey
) : SignedStreamableWriter {
override fun write(out: OutputStream) {
out.write(item.ripeTag)
}
override fun write(buffer: ByteBuffer) {
buffer.put(item.ripeTag)
}
override fun writeBytesToSign(out: OutputStream) = Unit // nothing to sign
}
companion object {
@JvmStatic
fun read(input: InputStream, stream: Long, length: Int, version: Long): GetPubkey {
return GetPubkey(version, stream, Decode.bytes(input, length))
}
}
}
@@ -0,0 +1,111 @@
/*
* 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.entity.SignedStreamableWriter
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 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 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() = stream.toInt()
override fun writer(): SignedStreamableWriter = Writer(this)
private class Writer(
private val item: Msg
) : SignedStreamableWriter {
val encryptedDataWriter = item.encrypted?.writer()
override fun write(out: OutputStream) {
encryptedDataWriter?.write(out) ?: throw IllegalStateException("Msg must be signed and encrypted before writing it.")
}
override fun write(buffer: ByteBuffer) {
encryptedDataWriter?.write(buffer) ?: throw IllegalStateException("Msg must be signed and encrypted before writing it.")
}
override fun writeBytesToSign(out: OutputStream) {
item.plaintext?.writer(false)?.write(out) ?: throw IllegalStateException("no plaintext data available")
}
}
companion object {
val ACK_LENGTH = 32
@JvmStatic
fun read(input: InputStream, stream: Long, length: Int) = Msg(stream, CryptoBox.read(input, length))
}
}
@@ -0,0 +1,41 @@
/*
* 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.SignedStreamable
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) : SignedStreamable {
abstract val type: ObjectType?
abstract val stream: Long
open val isSigned: Boolean = false
/**
* @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,108 @@
/*
* 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.entity.EncryptedStreamableWriter
import ch.dissem.bitmessage.entity.SignedStreamableWriter
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
abstract override fun writer(): EncryptedStreamableWriter
/**
* 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,120 @@
/*
* 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.EncryptedStreamableWriter
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 writer(): EncryptedStreamableWriter = Writer(this)
protected open class Writer(
private val item: V2Pubkey
) : EncryptedStreamableWriter {
override fun write(out: OutputStream) {
Encode.int32(item.behaviorBitfield, out)
out.write(item.signingKey, 1, 64)
out.write(item.encryptionKey, 1, 64)
}
override fun write(buffer: ByteBuffer) {
Encode.int32(item.behaviorBitfield, buffer)
buffer.put(item.signingKey, 1, 64)
buffer.put(item.encryptionKey, 1, 64)
}
override fun writeBytesToSign(out: OutputStream) {
// Nothing to do
}
override fun writeUnencrypted(out: OutputStream) {
write(out)
}
override fun writeUnencrypted(buffer: ByteBuffer) {
write(buffer)
}
}
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(input: InputStream, stream: Long): V2Pubkey {
return V2Pubkey(
version = 2,
stream = stream,
behaviorBitfield = Decode.uint32(input).toInt(),
signingKey = Decode.bytes(input, 64),
encryptionKey = Decode.bytes(input, 64)
)
}
}
}
@@ -0,0 +1,158 @@
/*
* 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.EncryptedStreamableWriter
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 val isSigned: Boolean = true
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)
}
override fun writer(): EncryptedStreamableWriter = Writer(this)
protected open class Writer(
private val item: V3Pubkey
) : V2Pubkey.Writer(item) {
override fun write(out: OutputStream) {
writeBytesToSign(out)
Encode.varBytes(
item.signature ?: throw IllegalStateException("signature not available"),
out
)
}
override fun write(buffer: ByteBuffer) {
super.write(buffer)
Encode.varInt(item.nonceTrialsPerByte, buffer)
Encode.varInt(item.extraBytes, buffer)
Encode.varBytes(
item.signature ?: throw IllegalStateException("signature not available"),
buffer
)
}
override fun writeBytesToSign(out: OutputStream) {
super.write(out)
Encode.varInt(item.nonceTrialsPerByte, out)
Encode.varInt(item.extraBytes, out)
}
}
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(input: InputStream, stream: Long): V3Pubkey {
return V3Pubkey(
version = 3,
stream = stream,
behaviorBitfield = Decode.int32(input),
signingKey = Decode.bytes(input, 64),
encryptionKey = Decode.bytes(input, 64),
nonceTrialsPerByte = Decode.varInt(input),
extraBytes = Decode.varInt(input),
signature = Decode.varBytes(input)
)
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.entity.payload
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.SignedStreamableWriter
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 writer(): SignedStreamableWriter = Writer(this)
protected open class Writer(
private val item: V4Broadcast
) : SignedStreamableWriter {
override fun writeBytesToSign(out: OutputStream) {
item.plaintext?.writer(false)?.write(out) ?: throw IllegalStateException("no plaintext data available")
}
override fun write(out: OutputStream) {
item.encrypted?.writer()?.write(out) ?: throw IllegalStateException("broadcast not encrypted")
}
override fun write(buffer: ByteBuffer) {
item.encrypted?.writer()?.write(buffer) ?: throw IllegalStateException("broadcast not encrypted")
}
}
companion object {
@JvmStatic
fun read(input: InputStream, stream: Long, length: Int) =
V4Broadcast(4, stream, CryptoBox.read(input, length), null)
}
}
@@ -0,0 +1,148 @@
/*
* 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.EncryptedStreamableWriter
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 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
}
override fun writer(): EncryptedStreamableWriter = Writer(this)
private class Writer(
val item: V4Pubkey
) : EncryptedStreamableWriter {
override fun write(out: OutputStream) {
out.write(item.tag)
item.encrypted?.writer()?.write(out) ?: throw IllegalStateException("pubkey is encrypted")
}
override fun write(buffer: ByteBuffer) {
buffer.put(item.tag)
item.encrypted?.writer()?.write(buffer) ?: throw IllegalStateException("pubkey is encrypted")
}
override fun writeUnencrypted(out: OutputStream) {
item.decrypted?.writer()?.write(out) ?: throw IllegalStateException("pubkey is encrypted")
}
override fun writeUnencrypted(buffer: ByteBuffer) {
item.decrypted?.writer()?.write(buffer) ?: throw IllegalStateException("pubkey is encrypted")
}
override fun writeBytesToSign(out: OutputStream) {
out.write(item.tag)
item.decrypted?.writer()?.writeBytesToSign(out) ?: throw IllegalStateException("pubkey is encrypted")
}
}
companion object {
@JvmStatic
fun read(input: InputStream, stream: Long, length: Int, encrypted: Boolean) = if (encrypted) {
V4Pubkey(stream,
Decode.bytes(input, 32),
CryptoBox.read(input, length - 32))
} else {
V4Pubkey(V3Pubkey.read(input, stream))
}
}
}
@@ -0,0 +1,67 @@
/*
* 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.entity.SignedStreamableWriter
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 writer(): SignedStreamableWriter = Writer(this)
private class Writer(
private val item: V5Broadcast
) : V4Broadcast.Writer(item) {
override fun writeBytesToSign(out: OutputStream) {
out.write(item.tag)
super.writeBytesToSign(out)
}
override fun write(out: OutputStream) {
out.write(item.tag)
super.write(out)
}
}
companion object {
@JvmStatic
fun read(input: InputStream, stream: Long, length: Int) =
V5Broadcast(stream, Decode.bytes(input, 32), CryptoBox.read(input, 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,71 @@
/*
* 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.StreamableWriter
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 toString(): String {
return Strings.hex(hash)
}
override fun writer(): StreamableWriter = Writer(this)
private class Writer(
private val item: InventoryVector
) : StreamableWriter {
override fun write(out: OutputStream) {
out.write(item.hash)
}
override fun write(buffer: ByteBuffer) {
buffer.put(item.hash)
}
}
companion object {
@JvmStatic
fun fromHash(hash: ByteArray?): InventoryVector? {
return InventoryVector(
hash ?: return null
)
}
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.entity.valueobject
import java.io.Serializable
import java.util.*
data class Label(
private val label: String,
val type: Label.Type? = null,
/**
* RGBA representation for the color.
*/
var color: Int = 0,
var ord: Int = 1000
) : 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,207 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.entity.valueobject
import ch.dissem.bitmessage.entity.Streamable
import ch.dissem.bitmessage.entity.StreamableWriter
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.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
}
fun writer(light: Boolean): StreamableWriter = Writer(
item = this,
light = light
)
override fun writer(): StreamableWriter = Writer(
item = this
)
private class Writer(
private val item: NetworkAddress,
private val light: Boolean = false
) : StreamableWriter {
override fun write(out: OutputStream) {
if (!light) {
Encode.int64(item.time, out)
Encode.int32(item.stream, out)
}
Encode.int64(item.services, out)
out.write(item.IPv6)
Encode.int16(item.port, out)
}
override fun write(buffer: ByteBuffer) {
if (!light) {
Encode.int64(item.time, buffer)
Encode.int32(item.stream, buffer)
}
Encode.int64(item.services, buffer)
buffer.put(item.IPv6)
Encode.int16(item.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,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.valueobject
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Streamable
import ch.dissem.bitmessage.entity.StreamableWriter
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.
*/
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
) : 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()
)
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)
)
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()
override fun writer(): StreamableWriter = Writer(this)
private class Writer(
private val item: PrivateKey
) : StreamableWriter {
override fun write(out: OutputStream) {
Encode.varInt(item.pubkey.version, out)
Encode.varInt(item.pubkey.stream, out)
val baos = ByteArrayOutputStream()
item.pubkey.writer().writeUnencrypted(baos)
Encode.varInt(baos.size(), out)
out.write(baos.toByteArray())
Encode.varBytes(item.privateSigningKey, out)
Encode.varBytes(item.privateEncryptionKey, out)
}
override fun write(buffer: ByteBuffer) {
Encode.varInt(item.pubkey.version, buffer)
Encode.varInt(item.pubkey.stream, buffer)
try {
val baos = ByteArrayOutputStream()
item.pubkey.writer().writeUnencrypted(baos)
Encode.varBytes(baos.toByteArray(), buffer)
} catch (e: IOException) {
throw ApplicationException(e)
}
Encode.varBytes(item.privateSigningKey, buffer)
Encode.varBytes(item.privateEncryptionKey, buffer)
}
}
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 var nonceTrialsPerByte = InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE
internal var extraBytes = InternalContext.NETWORK_EXTRA_BYTES
internal var features: Array<out Pubkey.Feature> = 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<out Pubkey.Feature>): Builder {
this.features = features
return this
}
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
}
}
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(input: InputStream): PrivateKey {
val version = Decode.varInt(input).toInt()
val stream = Decode.varInt(input)
val len = Decode.varInt(input).toInt()
val pubkey = Factory.readPubkey(version.toLong(), stream, input, len, false) ?: throw ApplicationException("Unknown pubkey version encountered")
val signingKey = Decode.varBytes(input)
val encryptionKey = Decode.varBytes(input)
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> = emptyList(),
val files: List<Attachment> = emptyList()
) : ExtendedEncoding.ExtendedType {
override val type: String = TYPE
override fun pack(): MPMap<MPString, MPType<*>> {
val result = MPMap<MPString, MPType<*>>()
result["".mp] = TYPE.mp
result["subject".mp] = subject.mp
result["body".mp] = body.mp
if (!files.isEmpty()) {
val items = MPArray<MPMap<MPString, MPType<*>>>()
result["files".mp] = items
for (file in files) {
val item = MPMap<MPString, MPType<*>>()
item["name".mp] = file.name.mp
item["data".mp] = file.data.mp
item["type".mp] = file.type.mp
item["disposition".mp] = file.disposition.name.mp
items.add(item)
}
}
if (!parents.isEmpty()) {
val items = MPArray<MPBinary>()
result["parents".mp] = 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["subject".mp]) ?: ""
val body = str(map["body".mp]) ?: ""
val parents = LinkedList<InventoryVector>()
val files = LinkedList<Attachment>()
val mpParents = map["parents".mp] as? MPArray<*>
for (parent in mpParents ?: emptyList<MPArray<MPBinary>>()) {
parents.add(InventoryVector.fromHash(
(parent as? MPBinary)?.value ?: continue
) ?: continue)
}
val mpFiles = map["files".mp] as? MPArray<*>
for (item in mpFiles ?: emptyList<Any>()) {
if (item is MPMap<*, *>) {
val b = Attachment.Builder()
b.name(str(item["name".mp])!!)
b.data(
bin(item["data".mp] ?: continue) ?: continue
)
b.type(str(item["type".mp])!!)
val disposition = str(item["disposition".mp])
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)
const val TYPE = "message"
}
}
@@ -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 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, TYPE.mp)
result.put("msgId".mp, msgId.hash.mp)
result.put("vote".mp, vote.mp)
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["msgId".mp] as? MPBinary)?.value) ?: throw IllegalArgumentException("data doesn't contain proper msgId")
val vote = str(map["vote".mp]) ?: 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,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.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 ->
@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)
}
}
}
@@ -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, input: InputStream, length: Int, encrypted: Boolean): Pubkey? {
when (version.toInt()) {
2 -> return V2Pubkey.read(input, stream)
3 -> return V3Pubkey.read(input, stream)
4 -> return V4Pubkey.read(input, 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,180 @@
/*
* 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(input: InputStream): NetworkMessage? {
findMagic(input)
val command = getCommand(input)
val length = Decode.uint32(input).toInt()
if (length > 1600003) {
throw NodeException("Payload of $length bytes received, no more than 1600003 was expected.")
}
val checksum = Decode.bytes(input, 4)
val payloadBytes = Decode.bytes(input, length)
if (testChecksum(checksum, payloadBytes)) {
val payload = getPayload(command, ByteArrayInputStream(payloadBytes), length)
return payload?.let { NetworkMessage(payload) }
} else {
throw IOException("Checksum failed for message '$command'")
}
}
@JvmStatic
fun getPayload(command: String, stream: InputStream, length: Int): MessagePayload? = when (command) {
"version" -> parseVersion(stream)
"verack" -> VerAck()
"addr" -> Addr(parseList(stream) { parseAddress(it, false) })
"inv" -> Inv(parseList(stream) { parseInventoryVector(it) })
"getdata" -> GetData(parseList(stream) { parseInventoryVector(it) })
"object" -> readObject(stream, length)
"custom" -> readCustom(stream, length)
else -> {
LOG.debug("Unknown command: $command")
null
}
}
private fun readCustom(input: InputStream, length: Int): MessagePayload = CustomMessage.read(input, length)
@JvmStatic
fun readObject(input: InputStream, length: Int): ObjectMessage {
val counter = AccessCounter()
val nonce = Decode.bytes(input, 8, counter)
val expiresTime = Decode.int64(input, counter)
val objectType = Decode.uint32(input, counter)
val version = Decode.varInt(input, counter)
val stream = Decode.varInt(input, counter)
val data = Decode.bytes(input, length - counter.length())
val payload: ObjectPayload = try {
Factory.getObjectPayload(objectType, version, stream, ByteArrayInputStream(data), 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))
}
GenericPayload(version, stream, data)
}
return ObjectMessage(
nonce, expiresTime, payload, objectType, version, stream
)
}
private fun <T> parseList(stream: InputStream, reader: (InputStream) -> (T)): List<T> {
val count = Decode.varInt(stream)
val items = LinkedList<T>()
for (i in 0 until count) {
items.add(reader(stream))
}
return items
}
private fun parseVersion(stream: InputStream) = Version(
version = Decode.int32(stream),
services = Decode.int64(stream),
timestamp = Decode.int64(stream),
addrRecv = parseAddress(stream, true),
addrFrom = parseAddress(stream, true),
nonce = Decode.int64(stream),
userAgent = Decode.varString(stream),
streams = Decode.varIntList(stream)
)
private fun parseInventoryVector(stream: InputStream) = 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(
time, streamNumber, services, ipv6, port
)
}
private fun testChecksum(checksum: ByteArray, payload: ByteArray): Boolean {
val payloadChecksum = cryptography().sha512(payload)
return checksum.indices.none { checksum[it] != payloadChecksum[it] }
}
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(input: InputStream) {
var pos = 0
for (i in 0..1619999) {
val b = input.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,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.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 {
val buffer: ByteBuffer = ByteBuffer.allocate(MAX_PAYLOAD_SIZE)
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 update() {
if (state != ReaderState.DATA) {
buffer.flip()
}
var s = when (state) {
ReaderState.MAGIC -> magic()
ReaderState.HEADER -> header()
ReaderState.DATA -> data()
else -> ReaderState.WAIT_FOR_DATA
}
while (s != ReaderState.WAIT_FOR_DATA) {
s = when (state) {
ReaderState.MAGIC -> magic()
ReaderState.HEADER -> header()
ReaderState.DATA -> data(flip = false)
else -> ReaderState.WAIT_FOR_DATA
}
}
}
private fun magic(): ReaderState = if (!findMagicBytes(buffer)) {
buffer.compact()
ReaderState.WAIT_FOR_DATA
} else {
state = ReaderState.HEADER
ReaderState.HEADER
}
private fun header(): ReaderState {
if (buffer.remaining() < 20) {
buffer.compact()
return ReaderState.WAIT_FOR_DATA
}
command = getCommand(buffer)
length = Decode.uint32(buffer).toInt()
if (length > MAX_PAYLOAD_SIZE) {
throw NodeException(
"Payload of " + length + " bytes received, no more than " +
MAX_PAYLOAD_SIZE + " was expected."
)
}
buffer.get(checksum)
state = ReaderState.DATA
return ReaderState.DATA
}
private fun data(flip: Boolean = true): ReaderState {
if (flip) {
if (buffer.position() < length) {
return ReaderState.WAIT_FOR_DATA
} else {
buffer.flip()
}
} else if (buffer.remaining() < length) {
buffer.compact()
return ReaderState.WAIT_FOR_DATA
}
if (!testChecksum(buffer)) {
state = ReaderState.MAGIC
buffer.clear()
throw NodeException("Checksum failed for message '$command'")
}
try {
V3MessageFactory.getPayload(
command ?: throw IllegalStateException("command is null"),
ByteArrayInputStream(
buffer.array(),
buffer.arrayOffset() + buffer.position(), length
),
length
)?.let { messages.add(NetworkMessage(it)) }
} catch (e: IOException) {
throw NodeException(e.message)
} finally {
state = ReaderState.MAGIC
}
return ReaderState.MAGIC
}
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
}
private enum class ReaderState {
MAGIC, HEADER, DATA, WAIT_FOR_DATA
}
}
@@ -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.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) = doubleHash("SHA-512", data, length);
override fun doubleSha256(data: ByteArray, length: Int) = doubleHash("SHA-256", data, length);
private fun doubleHash(method: String, data: ByteArray, length: Int): ByteArray {
val mda = md(method)
mda.update(data, 0, length)
return mda.digest(mda.digest())
}
override fun ripemd160(vararg data: ByteArray): ByteArray {
return hash("RIPEMD160", *data)
}
override fun sha1(vararg data: ByteArray): ByteArray {
return hash("SHA-1", *data)
}
override fun randomBytes(length: Int): ByteArray {
val result = ByteArray(length)
RANDOM.nextBytes(result)
return result
}
override fun doProofOfWork(
objectMessage: ObjectMessage, nonceTrialsPerByte: Long,
extraBytes: Long, callback: ProofOfWorkEngine.Callback
) {
val initialHash = getInitialHash(objectMessage)
val target = getProofOfWorkTarget(
objectMessage,
max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE), max(extraBytes, NETWORK_EXTRA_BYTES)
)
ctx.proofOfWorkEngine.calculateNonce(initialHash, target, callback)
}
@Throws(InsufficientProofOfWorkException::class)
override fun checkProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) {
val target = getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
val value = doubleSha512(
objectMessage.nonce ?: throw ApplicationException("Object without nonce"),
getInitialHash(objectMessage)
)
if (Bytes.lt(target, value, 8)) {
throw InsufficientProofOfWorkException(target, value)
}
}
protected fun doSign(data: ByteArray, privKey: java.security.PrivateKey): ByteArray {
// TODO: change this to ALGORITHM_EVP_SHA256 once it's generally used in the network
val sig = Signature.getInstance(ALGORITHM_ECDSA_SHA1, provider)
sig.initSign(privKey)
sig.update(data)
return sig.sign()
}
protected fun doCheckSignature(data: ByteArray, signature: ByteArray, publicKey: PublicKey): Boolean {
for (algorithm in arrayOf(ALGORITHM_ECDSA_SHA1, ALGORITHM_EVP_SHA256)) {
val sig = Signature.getInstance(algorithm, provider)
sig.initVerify(publicKey)
sig.update(data)
if (sig.verify(signature)) {
return true
}
}
return false
}
override fun getInitialHash(objectMessage: ObjectMessage): ByteArray {
return sha512(objectMessage.payloadBytesWithoutNonce)
}
override fun getProofOfWorkTarget(
objectMessage: ObjectMessage,
nonceTrialsPerByte: Long,
extraBytes: Long
): ByteArray {
@Suppress("NAME_SHADOWING")
val nonceTrialsPerByte = if (nonceTrialsPerByte == 0L) NETWORK_NONCE_TRIALS_PER_BYTE else nonceTrialsPerByte
@Suppress("NAME_SHADOWING")
val extraBytes = if (extraBytes == 0L) NETWORK_EXTRA_BYTES else extraBytes
val TTL = BigInteger.valueOf(objectMessage.expiresTime - UnixTime.now)
val powLength = BigInteger.valueOf(objectMessage.payloadBytesWithoutNonce.size + extraBytes)
val denominator = BigInteger.valueOf(nonceTrialsPerByte)
.multiply(
powLength.add(
powLength.multiply(TTL).divide(TWO_POW_16)
)
)
return Bytes.expand(TWO_POW_64.divide(denominator).toByteArray(), 8)
}
private fun hash(algorithm: String, vararg data: ByteArray): ByteArray {
val mda = md(algorithm)
for (d in data) {
mda.update(d)
}
return mda.digest()
}
private fun md(algorithm: String): MessageDigest {
try {
return MessageDigest.getInstance(algorithm, provider)
} catch (e: GeneralSecurityException) {
throw ApplicationException(e)
}
}
override fun mac(key_m: ByteArray, data: ByteArray): ByteArray {
try {
val mac = Mac.getInstance("HmacSHA256", provider)
mac.init(SecretKeySpec(key_m, "HmacSHA256"))
return mac.doFinal(data)
} catch (e: GeneralSecurityException) {
throw ApplicationException(e)
}
}
override fun createPubkey(
version: Long, stream: Long, privateSigningKey: ByteArray, privateEncryptionKey: ByteArray,
nonceTrialsPerByte: Long, extraBytes: Long, vararg features: Pubkey.Feature
): Pubkey {
return Factory.createPubkey(
version, stream,
createPublicKey(privateSigningKey),
createPublicKey(privateEncryptionKey),
nonceTrialsPerByte, extraBytes, *features
)
}
override fun keyToBigInt(privateKey: ByteArray): BigInteger {
return BigInteger(1, privateKey)
}
override fun randomNonce(): Long {
return RANDOM.nextLong()
}
companion object {
protected val LOG = LoggerFactory.getLogger(Cryptography::class.java)!!
private val RANDOM = SecureRandom()
private val TWO = BigInteger.valueOf(2)
private val TWO_POW_64 = TWO.pow(64)!!
private val TWO_POW_16 = TWO.pow(16)!!
}
}
@@ -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.ports
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.utils.SqlStrings.join
abstract class AbstractLabelRepository : LabelRepository {
override fun getLabels(): List<Label> {
return find("1=1")
}
override fun getLabels(vararg types: Label.Type): List<Label> {
return find("type IN (${join(*types)})")
}
protected abstract fun find(where: String): List<Label>
}
@@ -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.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.utils.Collections.single
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 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")
} 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}'"))
}
/**
* Finds messages that have a specific label, with optional offset and limit. If the limit is set to 0,
* offset and limit are ignored.
*/
open fun findMessages(label: Label?, offset: Int = 0, limit: Int = 0) = if (label == null) {
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit)
} else {
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit)
}
override fun findMessages(label: Label?) = if (label == null) {
find("id NOT IN (SELECT message_id FROM Message_Label)")
} else {
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, offset: Int, limit: Int): List<Plaintext> {
return find("conversation=X'${conversationId.toString().replace("-", "")}'", offset, limit)
}
/**
* Finds messages that mach the given where statement, with optional offset and limit. If the limit is set to 0,
* offset and limit are ignored.
*/
protected abstract fun find(where: String, offset: Int = 0, limit: Int = 0): 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?
}
@@ -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)
@@ -0,0 +1,273 @@
/*
* 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)
@JvmSynthetic
fun doProofOfWork(objectMessage: ObjectMessage, nonceTrialsPerByte: Long,
extraBytes: Long, callback: (ByteArray, ByteArray) -> Unit) {
doProofOfWork(objectMessage, nonceTrialsPerByte, extraBytes, object : ProofOfWorkEngine.Callback {
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
callback.invoke(initialHash, nonce)
}
})
}
/**
* @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,104 @@
/*
* 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
var listener: ((message: Plaintext, added: Collection<Label>, removed: Collection<Label>) -> Unit)? = null
override fun setContext(context: InternalContext) {
ctx = context
}
override fun setLabels(msg: Plaintext) {
msg.status = RECEIVED
val labelsToAdd =
if (msg.type == BROADCAST) {
ctx.labelRepository.getLabels(Label.Type.BROADCAST, Label.Type.UNREAD)
} else {
ctx.labelRepository.getLabels(Label.Type.INBOX, Label.Type.UNREAD)
}
msg.addLabels(labelsToAdd)
listener?.invoke(msg, labelsToAdd, emptyList())
}
override fun markAsDraft(msg: Plaintext) {
msg.status = DRAFT
val labelsToAdd = ctx.labelRepository.getLabels(Label.Type.DRAFT)
msg.addLabels(labelsToAdd)
listener?.invoke(msg, labelsToAdd, emptyList())
}
override fun markAsSending(msg: Plaintext) {
if (msg.to != null && msg.to!!.pubkey == null) {
msg.status = PUBKEY_REQUESTED
} else {
msg.status = DOING_PROOF_OF_WORK
}
val labelsToRemove = msg.labels.filter { it.type == Label.Type.DRAFT }
msg.removeLabel(Label.Type.DRAFT)
val labelsToAdd = ctx.labelRepository.getLabels(Label.Type.OUTBOX)
msg.addLabels(labelsToAdd)
listener?.invoke(msg, labelsToAdd, labelsToRemove)
}
override fun markAsSent(msg: Plaintext) {
msg.status = SENT
val labelsToRemove = msg.labels.filter { it.type == Label.Type.OUTBOX }
msg.removeLabel(Label.Type.OUTBOX)
val labelsToAdd = ctx.labelRepository.getLabels(Label.Type.SENT)
msg.addLabels(labelsToAdd)
listener?.invoke(msg, labelsToAdd, labelsToRemove)
}
override fun markAsAcknowledged(msg: Plaintext) {
msg.status = SENT_ACKNOWLEDGED
}
override fun markAsRead(msg: Plaintext) {
val labelsToRemove = msg.labels.filter { it.type == Label.Type.UNREAD }
msg.removeLabel(Label.Type.UNREAD)
listener?.invoke(msg, emptyList(), labelsToRemove)
}
override fun markAsUnread(msg: Plaintext) {
val labelsToAdd = ctx.labelRepository.getLabels(Label.Type.UNREAD)
msg.addLabels(labelsToAdd)
listener?.invoke(msg, labelsToAdd, emptyList())
}
override fun delete(msg: Plaintext) {
val labelsToRemove = msg.labels.toSet()
msg.labels.clear()
val labelsToAdd = ctx.labelRepository.getLabels(Label.Type.TRASH)
msg.addLabels(labelsToAdd)
listener?.invoke(msg, labelsToAdd, labelsToRemove)
}
override fun archive(msg: Plaintext) {
val labelsToRemove = msg.labels.toSet()
msg.labels.clear()
listener?.invoke(msg, emptyList(), labelsToRemove)
}
}
@@ -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()
}
@@ -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.valueobject.Label
interface LabelRepository {
fun getLabels(): List<Label>
fun getLabels(vararg types: Label.Type): List<Label>
fun save(label: Label)
}
@@ -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,72 @@
/*
* 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 countUnread(label: Label?): Int
fun getAllMessages(): List<Plaintext>
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?, offset: Int = 0, limit: Int = 0): 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, offset: Int = 0, limit: Int = 0): 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,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.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>)
fun update(node: NetworkAddress)
fun remove(node: NetworkAddress)
/**
* Remove stale nodes
*/
fun cleanup()
}
@@ -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 { input ->
val scanner = Scanner(input)
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()
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,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.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 initial hash and the calculated nonce as argument. The ProofOfWorkEngine
* implementation must make sure this is only called once.
*/
fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: Callback)
/**
* 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 initial hash and the calculated nonce as argument. The ProofOfWorkEngine
* implementation must make sure this is only called once.
*/
@JvmSynthetic
fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: (ByteArray, ByteArray) -> Unit) {
calculateNonce(initialHash, target, object : Callback {
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
callback.invoke(initialHash, nonce)
}
})
}
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)
}
}
}

Some files were not shown because too many files have changed in this diff Show More