Compare commits

...

176 Commits

Author SHA1 Message Date
Christian Basler c435e2f79e gradle wrapper should not require bash
continuous-integration/drone/push Build is failing Details
2021-12-08 14:39:47 +01:00
Christian Basler d5d3640991 Add '.drone.yml'
continuous-integration/drone/push Build is failing Details
2021-12-08 10:39:25 +01:00
Christian Basler 64ee41aee8 Merge branch 'release/2.0.4' 2016-11-01 06:28:44 +01:00
Christian Basler 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
Christian Basler 7b14081c63 Set 'send time' on outgoing messages 2016-10-28 07:52:05 +02:00
Christian Basler e1173d0619 Merge tag '2.0.3' into develop
Version 2.0.3
2016-10-24 08:13:18 +02:00
Christian Basler f0a5a40edd Merge branch 'release/2.0.3' 2016-10-24 08:12:48 +02:00
Christian Basler 1bc82cdd7d Merge branch 'master' into release/2.0.3 2016-10-22 09:37:56 +02:00
Christian Basler a880a8c10b Fixed NPE 2016-10-22 07:24:49 +02:00
Christian Basler 6a5fe01860 Merge tag '2.0.2' into develop
Version 2.0.2
2016-10-15 18:02:18 +02:00
Christian Basler 5cf6d308f2 Merge branch 'release/2.0.2' 2016-10-15 18:02:06 +02:00
Christian Basler 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
Christian Basler 5043e9ed03 Merge tag '2.0.1' into develop
Version 2.0.1
2016-10-07 22:10:16 +02:00
Christian Basler 15c6540e16 Merge branch 'release/2.0.1' 2016-10-07 22:10:00 +02:00
Christian Basler 784ed9ed4e Fixed importer exception on Android 2016-10-07 22:08:55 +02:00
Christian Basler 3a0555e6e9 Merge tag '2.0.0' into develop
Version 2.0.0
2016-10-02 23:26:20 +02:00
Christian Basler e71f30736d Merge branch 'release/2.0.0' 2016-10-02 23:22:34 +02:00
Christian Basler 503e665c5b Updated README.md 2016-10-02 23:16:18 +02:00
Christian Basler 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
Christian Basler 1003e7a582 Merge branch 'feature/nio' into develop 2016-09-21 19:38:48 +02:00
Christian Basler 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
Christian Basler 7e201dd2cf Change version of 'develop' branch to 'development-SNAPSHOT' 2016-09-13 07:36:28 +02:00
Christian Basler 83abce0f52 Deprecate DefaultNetworkHandler in favor of NioNetworkHandler 2016-09-13 07:25:23 +02:00
Christian Basler 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
Christian Basler 71124d7b01 Fixed tests 2016-09-12 10:02:52 +02:00
Christian Basler 489b8968e0 Refactored use of the DefaultMessageListener so it's retrieved from the InternalContext 2016-09-12 08:18:30 +02:00
Christian Basler a240606909 Minor improvements and fixes 2016-09-05 19:35:36 +02:00
Christian Basler 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
Christian Basler 827973f642 Improved connecting to the network 2016-08-29 12:30:26 +02:00
Christian Basler 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
Christian Basler 102d63e2c6 Fixed message count per label 2016-08-25 08:50:06 +02:00
Christian Basler 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
Christian Basler 3a92bab9ba Merge remote-tracking branch 'origin/develop' into feature/nio 2016-08-17 07:46:52 +02:00
Christian Basler 1eac644813 Fixed error in AbstractMessageRepository (archived messages couldn't be found) 2016-08-15 11:52:43 +02:00
Christian Basler 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
Christian Basler 631e71bc74 Updated gradle
(cherry picked from commit 52422d3)
2016-08-12 21:58:36 +02:00
Christian Basler 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
Christian Basler 52422d3398 Updated gradle 2016-08-12 11:04:04 +02:00
Christian Basler cd3a801704 Used wrong nonce for version message 2016-08-09 19:50:11 +02:00
Christian Basler 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
Christian Basler 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
Christian Basler 334a510743 Fixes, improved tests and other improvements 2016-07-29 07:49:53 +02:00
Christian Basler 56ebb7b8fa Better memory management for the out buffer 2016-07-27 07:38:39 +02:00
Christian Basler 48ff975ffd Better memory management for the in buffer (the same TODO for the out buffer. 2016-07-25 07:52:27 +02:00
Christian Basler 82ee4d05bb Raised test timeout, as the Jacoco run seems to be considerably slower. 2016-07-10 06:55:24 +02:00
Christian Basler 50f2c7e080 Fixed synchronisation 2016-07-09 16:37:12 +02:00
Christian Basler d130080df2 Implemented methods offer and request, system test works now but synchronization is still broken. 2016-07-08 18:14:41 +02:00
Christian Basler abc2f63aa6 Some further fixes and improvements, not all tests working yet 2016-06-20 16:33:47 +02:00
Christian Basler ae2120675f Tests with NioNetworkHandler as peer work now 2016-06-18 23:09:23 +02:00
Christian Basler 0fadb40c6c Improved tests and fixed some 2016-06-16 19:47:59 +02:00
Christian Basler ed4fd1002b Improved uint reading 2016-06-12 20:53:05 +02:00
Christian Basler 62d40fb2c3 Improved unsigned byte comparison 2016-06-12 20:43:23 +02:00
Christian Basler 12fb794203 Minor test improvements 2016-06-09 17:46:21 +02:00
Christian Basler cde4f7b3ce Some refactoring to move some common code into an AbstractConnection 2016-06-01 17:38:49 +02:00
Christian Basler 425a9dd6bf Merge branch 'develop' into feature/nio 2016-05-28 11:05:39 +02:00
Christian Basler c1fa642b4e Made tests more stable, albeit slightly slower 2016-05-28 11:04:47 +02:00
Christian Basler 08f2d5d6f1 Added write(ByteBuffer) to Streamable interface and a first draft for a NioNetworkHandler 2016-05-28 10:22:47 +02:00
Christian Basler b8f88b02d1 Improved tests 2016-05-26 22:50:37 +02:00
Christian Basler 5c4892d153 Added test 2016-05-26 06:55:31 +02:00
Christian Basler 3d2cea91ce Merge branch 'feature/ACK' into develop 2016-05-24 19:36:41 +02:00
Christian Basler 22108527f3 Minor update to the README file 2016-05-24 19:35:41 +02:00
Christian Basler 725d2b848e Fixed migration and added resend and cleanup options to demo application 2016-05-24 17:19:29 +02:00
Christian Basler 409dccd0be Fixed broken JavaDoc and removed unused import 2016-05-24 07:45:34 +02:00
Christian Basler ed6344c662 Added BitmessageContext method to resent unacknowledged messages and updated README.md 2016-05-23 20:11:44 +02:00
Christian Basler 14849a82ea Refactored JdbcMessageRepository so that alternative implementations can be done easier 2016-05-20 23:58:08 +02:00
Christian Basler c3d8a07e83 Added unit tests and fixed bug 2016-05-20 23:00:27 +02:00
Christian Basler 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
Christian Basler e44dd967d0 Test for NodeRegistry 2016-05-13 12:25:04 +02:00
Christian Basler a67ac27921 Fixed yet another test 2016-05-10 07:26:25 +02:00
Christian Basler 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
Christian Basler de8f04e22a Added warning to Labeler for developers who want to implement it. 2016-05-06 17:29:39 +02:00
Christian Basler 4f0b2cb8f8 Fixed system test (this time for real) 2016-05-06 14:13:39 +02:00
Christian Basler 678a48ac3f Fixed system test and ProofOfWorkService 2016-05-05 10:50:22 +02:00
Christian Basler 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
Christian Basler 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
Christian Basler 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
Christian Basler 784b192bab Simplyfied MultiThreadedPOWEngine and thread pool creation 2016-04-28 07:15:48 +02:00
Christian Basler a0505f5704 Minor improvements to the demo Application and a fix for when the ACK is empty 2016-04-25 08:13:46 +02:00
Christian Basler 61890b3da9 Some improvements to the application 2016-04-15 17:18:11 +02:00
Christian Basler ddd5826f42 Fixed feature bitfield calculation/resolution 2016-04-13 07:32:35 +02:00
Christian Basler c31ec7a9e5 Fixed problems after merging 2016-04-12 17:27:19 +02:00
Christian Basler 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
Christian Basler 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
Christian Basler 4f7f80c12a Added tests and code improvements 2016-04-08 19:22:40 +02:00
Christian Basler f70c15da38 Some code for deterministic addresses - needs tests 2016-04-07 23:01:16 +02:00
Christian Basler 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
Christian Basler ead5341b2e Some code for supporting chans 2016-03-31 20:04:23 +02:00
Christian Basler 3e5e431d6f Code cleanup 2016-02-28 23:03:00 +01:00
Christian Basler 57057298a1 Code cleanup 2016-02-26 16:30:45 +01:00
Christian Basler 9ca28ead66 Code cleanup 2016-02-26 16:12:43 +01:00
Christian Basler 2a17e6024f Code cleanup 2016-02-26 15:06:47 +01:00
Christian Basler bc68a5d3ec Code cleanup & improvements
- most notably removed some unnecessary synchronize blocks in the DefaultNetworkHandler
2016-02-26 14:34:08 +01:00
Christian Basler 382cb80a87 Updated badges 2016-02-26 14:01:26 +01:00
Christian Basler f6add5b2ea Code cleanup 2016-02-25 16:36:43 +01:00
Christian Basler 0a00a0a1b4 Code cleanup 2016-02-24 23:25:55 +01:00
Christian Basler 9f1e0057c9 Code cleanup 2016-02-24 23:10:04 +01:00
Christian Basler 4dd639e651 Code cleanup 2016-02-24 22:51:35 +01:00
Christian Basler f5215be8c6 Merge remote-tracking branch 'origin/master' into develop
# Conflicts:
#	README.md
2016-02-24 19:43:21 +01:00
Christian Basler a72718d4e8 Update README.md 2016-02-24 16:53:11 +01:00
Christian Basler 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
Christian Basler 86accb94f2 Update README.md
added link to IRC channel (thanks, kiwiirc!)
2016-02-24 14:34:14 +01:00
Christian Basler ccb102efd7 Update README.md 2016-02-15 16:54:13 +01:00
Christian Basler f71671e04a Added tests for DefaultMessageListener and ProofOfWorkService
and some minor improvements
2016-02-15 07:33:38 +01:00
Christian Basler e4a69f42b0 Fixed problem with sending broadcasts
(while adding some tests)
2016-02-13 08:03:05 +01:00
Christian Basler af3e63f592 Improved tests for cryptography 2016-02-09 17:09:22 +01:00
Christian Basler 60adf73616 Improved tests for repositories 2016-02-07 23:36:35 +01:00
Christian Basler 9c375d6608 Update README.md 2016-02-06 17:27:12 +01:00
Christian Basler 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
Christian Basler 06dbfbf64a Update README.md 2016-02-04 13:01:03 +01:00
Christian Basler 58e9644ff1 Merge tag '1.0.1' into develop
1.0.1 1.0.1
2016-02-04 10:18:40 +01:00
Christian Basler d580d6983b Merge branch 'release/1.0.1' 2016-02-04 10:18:39 +01:00
Christian Basler 9231cf5eaa Skip system tests by default as they don't work on Travis CI 2016-02-04 10:12:43 +01:00
Christian Basler 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
Christian Basler 2bfeedc7a9 Update build.gradle
Another try for codecov.io
2016-02-03 17:32:04 +01:00
Christian Basler ea700755b6 Update .travis.yml
Another try
2016-02-03 17:17:31 +01:00
Christian Basler 5ab577f18a Update .travis.yml to work with codecov.io 2016-02-03 17:09:26 +01:00
Christian Basler b1599cbd60 This should fix the build on Travis CI / machines that don't have signing configured 2016-02-03 07:52:22 +01:00
Christian Basler 91c41fa3bd Version 1.0.1 bump 2016-02-02 21:23:15 +01:00
Christian Basler 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
Christian Basler edd8045327 Fixed / improved message sending and added a system test (it's a start) 2016-02-02 20:40:01 +01:00
Christian Basler 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
Christian Basler 3103ae6edd Merge remote-tracking branch 'origin/master' into develop 2016-01-29 18:20:24 +01:00
Christian Basler 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
Christian Basler 3eabda29ee Merge branch 'master' into develop 2016-01-24 09:38:47 +01:00
Christian Basler 8bd7a245b0 Updated README.md 2016-01-24 09:38:12 +01:00
Christian Basler ae8a0ac0b9 Merge tag '1.0.0' into develop
Tagging version 1.0.0 1.0.0
2016-01-23 17:22:05 +01:00
Christian Basler e56d8c25e0 Merge branch 'release/1.0.0' 2016-01-23 17:22:03 +01:00
Christian Basler 9a91351091 Version 1.0.0 bump 2016-01-23 17:21:33 +01:00
Christian Basler 6c0eae5919 Added CONTRIBUTING.md 2016-01-23 17:19:36 +01:00
Christian Basler 07b349563f Fixed an issue in the POW engine 2016-01-23 17:18:25 +01:00
Christian Basler 9f05af8bb7 Finally fixed the bug that was haunting me for the last week. 2016-01-21 20:32:23 +01:00
Christian Basler 733335ef42 Improved performance and network stability 2016-01-19 21:09:46 +01:00
Christian Basler e29310102f Updated UML diagram 2016-01-19 21:07:26 +01:00
Christian Basler 35077243b0 Fixed synchronization 2016-01-17 07:13:29 +01:00
Christian Basler ac6f291964 Renamed module 'domain' to 'core' to make its purpose more clear 2016-01-17 05:42:49 +01:00
Christian Basler 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
Christian Basler 549c8854ed Refactoring: renamed 'security' to 'cryptography' 2016-01-10 13:38:32 +01:00
Christian Basler de0100e14f Merge branch 'feature/server-pow' into develop 2016-01-10 12:22:57 +01:00
Christian Basler fad3e07871 Some changes needed for POW server and some general improvements 2015-12-21 15:13:48 +01:00
Christian Basler 61788802c5 Some POW improvements 2015-12-18 16:42:17 +01:00
Christian Basler 51bf3b8bd2 Fixed tests 2015-12-12 11:05:13 +01:00
Christian Basler 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
Christian Basler 991a0e5f86 Some improvements for custom message handling 2015-12-02 17:45:50 +01:00
Christian Basler 99266712fa Some extensions for server POW 2015-11-28 20:27:05 +01:00
Christian Basler 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
Christian Basler 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
Christian Basler 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
Christian Basler c9c0806e0d Attempt to disconnect on thread interrupt 2015-10-29 12:34:29 +01:00
Christian Basler 9c2d8589bf Improved POW test 2015-10-28 16:56:09 +01:00
Christian Basler 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
Christian Basler 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
Christian Basler 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
Christian Basler a398b072b5 (probably) fixed another concurrency problem in the tests 2015-10-19 15:14:25 +02:00
Christian Basler fb300c8731 Fixed possible ConcurrentModificationException 2015-10-19 15:08:11 +02:00
Christian Basler 1e605f56a5 Added method to retrieve all properties 2015-10-18 18:22:49 +02:00
Christian Basler 3f1b41a2c1 Merge branch 'develop' of github.com:Dissem/Jabit into develop 2015-10-15 18:04:55 +02:00
Christian Basler ac70a4b632 We probably shouldn't leave the bitmessage node running after the test is finished 2015-10-15 17:59:32 +02:00
Christian Basler 4913a21b11 Fixed NetworkHandlerTest 2015-10-15 15:34:04 +02:00
Christian Basler 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
Christian Basler 409100ab20 Work-around for parsing problem
(and made code more robust for different parsing problems)
2015-10-14 22:56:46 +02:00
Christian Basler ddaa52f416 Merge branch 'feature/threaded-connections' into develop 2015-10-14 20:38:37 +02:00
Christian Basler 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
Christian Basler 117ac3ca73 Fixed some problems and added cleanup on shutdown 2015-10-12 12:44:13 +02:00
Christian Basler 3d1bd7227b Updated H2 version 2015-10-12 12:42:11 +02:00
Christian Basler ea1419eda1 Synchronisation API - added option to wait for the synchronization to finish 2015-10-08 13:12:39 +02:00
Christian Basler 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
Christian Basler c3fdee79ca Some bugfixes and added findMessages by sender 2015-09-29 07:13:27 +02:00
Christian Basler 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
Christian Basler 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
Christian Basler 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
Christian Basler 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
Christian Basler 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
Christian Basler 6542bd1451 now the build should work for anyone (esp. travis) 2015-07-04 11:13:35 +02:00
Christian Basler 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
Christian Basler bef9ea716e Merge branch 'release/0.2.0' into develop 2015-07-03 14:48:34 +02:00
196 changed files with 11404 additions and 3465 deletions

9
.drone.yml Normal file
View File

@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: test
image: adoptopenjdk/openjdk8:alpine
commands:
- ./gradlew assemble
- ./gradlew check

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
### Gradle ###
.gradle
build/
classes/
# Ignore Gradle GUI config
gradle-app.setting

View File

@ -1,2 +1,10 @@
language: java
sudo: false # faster builds
jdk:
- oraclejdk8
before_install:
- pip install --user codecov
after_success:
- codecov

File diff suppressed because it is too large Load Diff

24
CONTRIBUTING.md Normal file
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.

View File

@ -1,9 +1,32 @@
Jabit [![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit)
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 `gradle build` or `./gradlew build`.
A Java implementation for the Bitmessage protocol. To build, use command `./gradlew build`.
Please note that development is still heavily in progress, and I will break the database a lot until it's ready for prime time.
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
--------
@ -15,7 +38,7 @@ Project Status
Basically, everything needed for a working Bitmessage client is there:
* Creating new identities (private addresses)
* Adding contracts and subscriptions
* Adding contacts and subscriptions
* Receiving broadcasts
* Receiving messages
* Sending messages and broadcasts
@ -27,20 +50,27 @@ Basically, everything needed for a working Bitmessage client is there:
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-domain:0.2.0'
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:0.2.0'
compile 'ch.dissem.jabit:jabit-repositories:0.2.0'
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:0.2.0'
compile "ch.dissem.jabit:jabit-wif:$jabitVersion"
```
For Android clients use `jabit-cryptography-spongy` instead of `jabit-cryptography-bouncy`.
Usage
-----
@ -51,19 +81,26 @@ BitmessageContext ctx = new BitmessageContext.Builder()
.addressRepo(new JdbcAddressRepository(jdbcConfig))
.inventory(new JdbcInventory(jdbcConfig))
.messageRepo(new JdbcMessageRepository(jdbcConfig))
.nodeRegistry(new MemoryNodeRegistry())
.networkHandler(new NetworkNode())
.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. Next you'll need to
start the context and decide what happens if a message arrives:
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
ctx.startup(new BitmessageContext.Listener() {
@Override
public void receive(Plaintext plaintext) {
// TODO: Notify the user
}
});
.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
@ -79,3 +116,22 @@ 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();
```

View File

@ -2,17 +2,22 @@ subprojects {
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'jacoco'
apply plugin: 'gitflow-version'
sourceCompatibility = 1.7
group = 'ch.dissem.jabit'
version = '0.2.0'
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
repositories {
mavenCentral()
}
test {
testLogging {
exceptionFormat = 'full'
}
}
task javadocJar(type: Jar) {
classifier = 'javadoc'
from javadoc
@ -28,7 +33,7 @@ subprojects {
}
signing {
required { isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives") }
required { isRelease && project.getProperties().get("signing.keyId")?.length() > 0 }
sign configurations.archives
}
@ -37,11 +42,6 @@ subprojects {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
if (!hasProperty('ossrhUsername')) {
ext.ossrhUsername = 'dummy'
ext.ossrhPassword = 'dummy'
}
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
@ -78,4 +78,13 @@ subprojects {
}
}
}
}
jacocoTestReport {
reports {
xml.enabled = true
html.enabled = true
}
}
check.dependsOn jacocoTestReport
}

View File

@ -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') << {
println "Version deduced from git: '${project.version}'"
}
}
}

View File

@ -0,0 +1 @@
implementation-class=ch.dissem.gradle.GitFlowVersion

32
core/build.gradle Normal file
View File

@ -0,0 +1,32 @@
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:1.7.12'
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile project(':cryptography-bc')
}

View File

@ -0,0 +1,451 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Property;
import ch.dissem.bitmessage.utils.TTL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES;
import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.UnixTime.*;
/**
* <p>Use this class if you want to create a Bitmessage client.</p>
* You'll need the Builder to create a BitmessageContext, and set the following properties:
* <ul>
* <li>addressRepo</li>
* <li>inventory</li>
* <li>nodeRegistry</li>
* <li>networkHandler</li>
* <li>messageRepo</li>
* <li>streams</li>
* </ul>
* <p>The default implementations in the different module builds can be used.</p>
* <p>The port defaults to 8444 (the default Bitmessage port)</p>
*/
public class BitmessageContext {
public static final int CURRENT_VERSION = 3;
private final static Logger LOG = LoggerFactory.getLogger(BitmessageContext.class);
private final InternalContext ctx;
private final Labeler labeler;
private final boolean sendPubkeyOnIdentityCreation;
private BitmessageContext(Builder builder) {
if (builder.listener instanceof Listener.WithContext) {
((Listener.WithContext) builder.listener).setContext(this);
}
ctx = new InternalContext(builder);
labeler = builder.labeler;
ctx.getProofOfWorkService().doMissingProofOfWork(30_000); // TODO: this should be configurable
sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation;
}
public AddressRepository addresses() {
return ctx.getAddressRepository();
}
public MessageRepository messages() {
return ctx.getMessageRepository();
}
public Labeler labeler() {
return labeler;
}
public BitmessageAddress createIdentity(boolean shorter, Feature... features) {
final BitmessageAddress identity = new BitmessageAddress(new PrivateKey(
shorter,
ctx.getStreams()[0],
NETWORK_NONCE_TRIALS_PER_BYTE,
NETWORK_EXTRA_BYTES,
features
));
ctx.getAddressRepository().save(identity);
if (sendPubkeyOnIdentityCreation) {
ctx.sendPubkey(identity, identity.getStream());
}
return identity;
}
public BitmessageAddress joinChan(String passphrase, String address) {
BitmessageAddress chan = BitmessageAddress.chan(address, passphrase);
chan.setAlias(passphrase);
ctx.getAddressRepository().save(chan);
return chan;
}
public BitmessageAddress createChan(String passphrase) {
// FIXME: hardcoded stream number
BitmessageAddress chan = BitmessageAddress.chan(1, passphrase);
ctx.getAddressRepository().save(chan);
return chan;
}
public List<BitmessageAddress> createDeterministicAddresses(
String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) {
List<BitmessageAddress> result = BitmessageAddress.deterministic(
passphrase, numberOfAddresses, version, stream, shorter);
for (int i = 0; i < result.size(); i++) {
BitmessageAddress address = result.get(i);
address.setAlias("deterministic (" + (i + 1) + ")");
ctx.getAddressRepository().save(address);
}
return result;
}
public void broadcast(final BitmessageAddress from, final String subject, final String message) {
Plaintext msg = new Plaintext.Builder(BROADCAST)
.from(from)
.message(subject, message)
.build();
send(msg);
}
public void send(final BitmessageAddress from, final BitmessageAddress to, final String subject, final String message) {
if (from.getPrivateKey() == null) {
throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key.");
}
Plaintext msg = new Plaintext.Builder(MSG)
.from(from)
.to(to)
.message(subject, message)
.build();
send(msg);
}
public void send(final Plaintext msg) {
if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) {
throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key.");
}
labeler().markAsSending(msg);
BitmessageAddress to = msg.getTo();
if (to != null) {
if (to.getPubkey() == null) {
LOG.info("Public key is missing from recipient. Requesting.");
ctx.requestPubkey(to);
}
if (to.getPubkey() == null) {
ctx.getMessageRepository().save(msg);
}
}
if (to == null || to.getPubkey() != null) {
LOG.info("Sending message.");
ctx.getMessageRepository().save(msg);
if (msg.getType() == MSG) {
ctx.send(msg);
} else {
ctx.send(
msg.getFrom(),
to,
Factory.getBroadcast(msg),
msg.getTTL()
);
}
}
}
public void startup() {
ctx.getNetworkHandler().start();
}
public void shutdown() {
ctx.getNetworkHandler().stop();
}
/**
* @param host a trusted node that must be reliable (it's used for every synchronization)
* @param port of the trusted host, default is 8444
* @param timeoutInSeconds synchronization should end no later than about 5 seconds after the timeout elapsed, even
* if not all objects were fetched
* @param wait waits for the synchronization thread to finish
*/
public void synchronize(InetAddress host, int port, long timeoutInSeconds, boolean wait) {
Future<?> future = ctx.getNetworkHandler().synchronize(host, port, timeoutInSeconds);
if (wait) {
try {
future.get();
} catch (InterruptedException e) {
LOG.info("Thread was interrupted. Trying to shut down synchronization and returning.");
future.cancel(true);
} catch (CancellationException | ExecutionException e) {
LOG.debug(e.getMessage(), e);
}
}
}
/**
* Send a custom message to a specific node (that should implement handling for this message type) and returns
* the response, which in turn is expected to be a {@link CustomMessage}.
*
* @param server the node's address
* @param port the node's port
* @param request the request
* @return the response
*/
public CustomMessage send(InetAddress server, int port, CustomMessage request) {
return ctx.getNetworkHandler().send(server, port, request);
}
/**
* Removes expired objects from the inventory. You should call this method regularly,
* e.g. daily and on each shutdown.
*/
public void cleanup() {
ctx.getInventory().cleanup();
}
/**
* Sends messages again whose time to live expired without being acknowledged. (And whose
* recipient is expected to send acknowledgements.
* <p>
* You should call this method regularly, but be aware of the following:
* <ul>
* <li>As messages might be sent, POW will be done. It is therefore not advised to
* call it on shutdown.</li>
* <li>It shouldn't be called right after startup, as it's possible the missing
* acknowledgement was sent while the client was offline.</li>
* <li>Other than that, the call isn't expensive as long as there is no message
* to send, so it might be a good idea to just call it every few minutes.</li>
* </ul>
*/
public void resendUnacknowledgedMessages() {
ctx.resendUnacknowledged();
}
public boolean isRunning() {
return ctx.getNetworkHandler().isRunning();
}
public void addContact(BitmessageAddress contact) {
ctx.getAddressRepository().save(contact);
if (contact.getPubkey() == null) {
ctx.requestPubkey(contact);
}
}
public void addSubscribtion(BitmessageAddress address) {
address.setSubscribed(true);
ctx.getAddressRepository().save(address);
tryToFindBroadcastsForAddress(address);
}
private void tryToFindBroadcastsForAddress(BitmessageAddress address) {
for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), Broadcast.getVersion(address), ObjectType.BROADCAST)) {
try {
Broadcast broadcast = (Broadcast) object.getPayload();
broadcast.decrypt(address);
// This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with
// other subscriptions and the interface stays as simple as possible.
ctx.getNetworkListener().receive(object);
} catch (DecryptionFailedException ignore) {
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
}
}
public Property status() {
return new Property("status", null,
ctx.getNetworkHandler().getNetworkStatus(),
new Property("unacknowledged", ctx.getMessageRepository().findMessagesToResend().size())
);
}
/**
* Returns the {@link InternalContext} - normally you wouldn't need it,
* unless you are doing something crazy with the protocol.
*/
public InternalContext internals() {
return ctx;
}
public interface Listener {
void receive(Plaintext plaintext);
/**
* A message listener that needs a {@link BitmessageContext}, i.e. for implementing some sort of chat bot.
*/
interface WithContext extends Listener {
void setContext(BitmessageContext ctx);
}
}
public static final class Builder {
int port = 8444;
Inventory inventory;
NodeRegistry nodeRegistry;
NetworkHandler networkHandler;
AddressRepository addressRepo;
MessageRepository messageRepo;
ProofOfWorkRepository proofOfWorkRepository;
ProofOfWorkEngine proofOfWorkEngine;
Cryptography cryptography;
CustomCommandHandler customCommandHandler;
Labeler labeler;
Listener listener;
int connectionLimit = 150;
long connectionTTL = 30 * MINUTE;
boolean sendPubkeyOnIdentityCreation = true;
public Builder port(int port) {
this.port = port;
return this;
}
public Builder inventory(Inventory inventory) {
this.inventory = inventory;
return this;
}
public Builder nodeRegistry(NodeRegistry nodeRegistry) {
this.nodeRegistry = nodeRegistry;
return this;
}
public Builder networkHandler(NetworkHandler networkHandler) {
this.networkHandler = networkHandler;
return this;
}
public Builder addressRepo(AddressRepository addressRepo) {
this.addressRepo = addressRepo;
return this;
}
public Builder messageRepo(MessageRepository messageRepo) {
this.messageRepo = messageRepo;
return this;
}
public Builder powRepo(ProofOfWorkRepository proofOfWorkRepository) {
this.proofOfWorkRepository = proofOfWorkRepository;
return this;
}
public Builder cryptography(Cryptography cryptography) {
this.cryptography = cryptography;
return this;
}
public Builder customCommandHandler(CustomCommandHandler handler) {
this.customCommandHandler = handler;
return this;
}
public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) {
this.proofOfWorkEngine = proofOfWorkEngine;
return this;
}
public Builder labeler(Labeler labeler) {
this.labeler = labeler;
return this;
}
public Builder listener(Listener listener) {
this.listener = listener;
return this;
}
public Builder connectionLimit(int connectionLimit) {
this.connectionLimit = connectionLimit;
return this;
}
public Builder connectionTTL(int hours) {
this.connectionTTL = hours * HOUR;
return this;
}
/**
* By default a client will send the public key when an identity is being created. On weaker devices
* this behaviour might not be desirable.
*/
public Builder doNotSendPubkeyOnIdentityCreation() {
this.sendPubkeyOnIdentityCreation = false;
return this;
}
/**
* Time to live in seconds for public keys the client sends. Defaults to the maximum of 28 days,
* but on weak devices smaller values might be desirable.
* <p>
* Please be aware that this might cause some problems where you can't receive a message (the
* sender can't receive your public key) in some special situations. Also note that it's probably
* not a good idea to set it too low.
* </p>
*
* @deprecated use {@link TTL#pubkey(long)} instead.
*/
public Builder pubkeyTTL(long days) {
if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days");
TTL.pubkey(days);
return this;
}
public BitmessageContext build() {
nonNull("inventory", inventory);
nonNull("nodeRegistry", nodeRegistry);
nonNull("networkHandler", networkHandler);
nonNull("addressRepo", addressRepo);
nonNull("messageRepo", messageRepo);
nonNull("proofOfWorkRepo", proofOfWorkRepository);
if (proofOfWorkEngine == null) {
proofOfWorkEngine = new MultiThreadedPOWEngine();
}
if (labeler == null) {
labeler = new DefaultLabeler();
}
if (customCommandHandler == null) {
customCommandHandler = new CustomCommandHandler() {
@Override
public MessagePayload handle(CustomMessage request) {
throw new IllegalStateException(
"Received custom request, but no custom command handler configured.");
}
};
}
return new BitmessageContext(this);
}
private void nonNull(String name, Object o) {
if (o == null) throw new IllegalStateException(name + " must not be null");
}
}
}

View File

@ -20,8 +20,9 @@ import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.ports.Labeler;
import ch.dissem.bitmessage.ports.NetworkHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,23 +31,34 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static ch.dissem.bitmessage.entity.Plaintext.Status.*;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED;
class DefaultMessageListener implements NetworkHandler.MessageListener {
class DefaultMessageListener implements NetworkHandler.MessageListener, InternalContext.ContextHolder {
private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class);
private final InternalContext ctx;
private final Labeler labeler;
private final BitmessageContext.Listener listener;
private InternalContext ctx;
public DefaultMessageListener(InternalContext context, BitmessageContext.Listener listener) {
this.ctx = context;
public DefaultMessageListener(Labeler labeler, BitmessageContext.Listener listener) {
this.labeler = labeler;
this.listener = listener;
}
@Override
public void setContext(InternalContext context) {
this.ctx = context;
}
@Override
@SuppressWarnings("ConstantConditions")
public void receive(ObjectMessage object) throws IOException {
ObjectPayload payload = object.getPayload();
if (payload.getType() == null) return;
if (payload.getType() == null) {
if (payload instanceof GenericPayload) {
receive((GenericPayload) payload);
}
return;
}
switch (payload.getType()) {
case GET_PUBKEY: {
@ -65,13 +77,17 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
receive(object, (Broadcast) payload);
break;
}
default: {
throw new IllegalArgumentException("Unknown payload type " + payload.getType());
}
}
}
protected void receive(ObjectMessage object, GetPubkey getPubkey) {
BitmessageAddress identity = ctx.getAddressRepo().findIdentity(getPubkey.getRipeTag());
if (identity != null && identity.getPrivateKey() != null) {
LOG.debug("Got pubkey request for identity " + identity);
BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag());
if (identity != null && identity.getPrivateKey() != null && !identity.isChan()) {
LOG.info("Got pubkey request for identity " + identity);
// FIXME: only send pubkey if it wasn't sent in the last 28 days
ctx.sendPubkey(identity, object.getStream());
}
}
@ -81,51 +97,43 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
try {
if (pubkey instanceof V4Pubkey) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
address = ctx.getAddressRepo().findContact(v4Pubkey.getTag());
address = ctx.getAddressRepository().findContact(v4Pubkey.getTag());
if (address != null) {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
}
} else {
address = ctx.getAddressRepo().findContact(pubkey.getRipe());
address = ctx.getAddressRepository().findContact(pubkey.getRipe());
}
if (address != null) {
address.setPubkey(pubkey);
LOG.debug("Got pubkey for contact " + address);
ctx.getAddressRepo().save(address);
List<Plaintext> messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address);
LOG.debug("Sending " + messages.size() + " messages for contact " + address);
for (Plaintext msg : messages) {
msg.setStatus(DOING_PROOF_OF_WORK);
ctx.getMessageRepository().save(msg);
ctx.send(
msg.getFrom(),
msg.getTo(),
new Msg(msg),
+2 * DAY,
ctx.getNonceTrialsPerByte(msg.getTo()),
ctx.getExtraBytes(msg.getTo())
);
msg.setStatus(SENT);
ctx.getMessageRepository().save(msg);
}
updatePubkey(address, pubkey);
}
} catch (DecryptionFailedException ignore) {
}
}
private void updatePubkey(BitmessageAddress address, Pubkey pubkey) {
address.setPubkey(pubkey);
LOG.info("Got pubkey for contact " + address);
ctx.getAddressRepository().save(address);
List<Plaintext> messages = ctx.getMessageRepository().findMessages(PUBKEY_REQUESTED, address);
LOG.info("Sending " + messages.size() + " messages for contact " + address);
for (Plaintext msg : messages) {
ctx.getLabeler().markAsSending(msg);
ctx.getMessageRepository().save(msg);
ctx.send(msg);
}
}
protected void receive(ObjectMessage object, Msg msg) throws IOException {
for (BitmessageAddress identity : ctx.getAddressRepo().getIdentities()) {
for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) {
try {
msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey());
msg.getPlaintext().setTo(identity);
if (!object.isSignatureValid(msg.getPlaintext().getFrom().getPubkey())) {
Plaintext plaintext = msg.getPlaintext();
plaintext.setTo(identity);
if (!object.isSignatureValid(plaintext.getFrom().getPubkey())) {
LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
} else {
msg.getPlaintext().setStatus(RECEIVED);
msg.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD));
msg.getPlaintext().setInventoryVector(object.getInventoryVector());
ctx.getMessageRepository().save(msg.getPlaintext());
listener.receive(msg.getPlaintext());
receive(object.getInventoryVector(), plaintext);
}
break;
} catch (DecryptionFailedException ignore) {
@ -133,9 +141,19 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
}
}
protected void receive(GenericPayload ack) {
if (ack.getData().length == Msg.ACK_LENGTH) {
Plaintext msg = ctx.getMessageRepository().getMessageForAck(ack.getData());
if (msg != null) {
ctx.getLabeler().markAsAcknowledged(msg);
ctx.getMessageRepository().save(msg);
}
}
}
protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException {
byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null;
for (BitmessageAddress subscription : ctx.getAddressRepo().getSubscriptions(broadcast.getVersion())) {
for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) {
if (tag != null && !Arrays.equals(tag, subscription.getTag())) {
continue;
}
@ -144,14 +162,26 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) {
LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
} else {
broadcast.getPlaintext().setStatus(RECEIVED);
broadcast.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
broadcast.getPlaintext().setInventoryVector(object.getInventoryVector());
ctx.getMessageRepository().save(broadcast.getPlaintext());
listener.receive(broadcast.getPlaintext());
receive(object.getInventoryVector(), broadcast.getPlaintext());
}
} catch (DecryptionFailedException ignore) {
}
}
}
protected void receive(InventoryVector iv, Plaintext msg) {
msg.setInventoryVector(iv);
labeler.setLabels(msg);
ctx.getMessageRepository().save(msg);
listener.receive(msg);
updatePubkey(msg.getFrom(), msg.getFrom().getPubkey());
if (msg.getType() == Plaintext.Type.MSG && msg.getTo().has(Pubkey.Feature.DOES_ACK)) {
ObjectMessage ack = msg.getAckMessage();
if (ack != null) {
ctx.getInventory().storeObject(ack);
ctx.getNetworkHandler().offer(ack.getInventoryVector());
}
}
}
}

View File

@ -0,0 +1,317 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.exception.ApplicationException;
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.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
/**
* 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.
* <p>
* On the other hand, if you need the BitmessageContext in a port implementation, the same thing might apply.
* </p>
*/
public class InternalContext {
private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class);
public final static long NETWORK_NONCE_TRIALS_PER_BYTE = 1000;
public final static long NETWORK_EXTRA_BYTES = 1000;
private final Cryptography cryptography;
private final Inventory inventory;
private final NodeRegistry nodeRegistry;
private final NetworkHandler networkHandler;
private final AddressRepository addressRepository;
private final MessageRepository messageRepository;
private final ProofOfWorkRepository proofOfWorkRepository;
private final ProofOfWorkEngine proofOfWorkEngine;
private final CustomCommandHandler customCommandHandler;
private final ProofOfWorkService proofOfWorkService;
private final Labeler labeler;
private final NetworkHandler.MessageListener networkListener;
private final TreeSet<Long> streams = new TreeSet<>();
private final int port;
private final long clientNonce;
private long connectionTTL;
private int connectionLimit;
public InternalContext(BitmessageContext.Builder builder) {
this.cryptography = builder.cryptography;
this.inventory = builder.inventory;
this.nodeRegistry = builder.nodeRegistry;
this.networkHandler = builder.networkHandler;
this.addressRepository = builder.addressRepo;
this.messageRepository = builder.messageRepo;
this.proofOfWorkRepository = builder.proofOfWorkRepository;
this.proofOfWorkService = new ProofOfWorkService();
this.proofOfWorkEngine = builder.proofOfWorkEngine;
this.clientNonce = cryptography.randomNonce();
this.customCommandHandler = builder.customCommandHandler;
this.port = builder.port;
this.connectionLimit = builder.connectionLimit;
this.connectionTTL = builder.connectionTTL;
this.labeler = builder.labeler;
this.networkListener = new DefaultMessageListener(labeler, builder.listener);
Singleton.initialize(cryptography);
// TODO: streams of new identities and subscriptions should also be added. This works only after a restart.
for (BitmessageAddress address : addressRepository.getIdentities()) {
streams.add(address.getStream());
}
for (BitmessageAddress address : addressRepository.getSubscriptions()) {
streams.add(address.getStream());
}
if (streams.isEmpty()) {
streams.add(1L);
}
init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, customCommandHandler, builder.labeler,
networkListener);
for (BitmessageAddress identity : addressRepository.getIdentities()) {
streams.add(identity.getStream());
}
}
private void init(Object... objects) {
for (Object o : objects) {
if (o instanceof ContextHolder) {
((ContextHolder) o).setContext(this);
}
}
}
public Cryptography getCryptography() {
return cryptography;
}
public Inventory getInventory() {
return inventory;
}
public NodeRegistry getNodeRegistry() {
return nodeRegistry;
}
public NetworkHandler getNetworkHandler() {
return networkHandler;
}
public AddressRepository getAddressRepository() {
return addressRepository;
}
public MessageRepository getMessageRepository() {
return messageRepository;
}
public ProofOfWorkRepository getProofOfWorkRepository() {
return proofOfWorkRepository;
}
public ProofOfWorkEngine getProofOfWorkEngine() {
return proofOfWorkEngine;
}
public ProofOfWorkService getProofOfWorkService() {
return proofOfWorkService;
}
public Labeler getLabeler() {
return labeler;
}
public NetworkHandler.MessageListener getNetworkListener() {
return networkListener;
}
public long[] getStreams() {
long[] result = new long[streams.size()];
int i = 0;
for (long stream : streams) {
result[i++] = stream;
}
return result;
}
public int getPort() {
return port;
}
public void send(final Plaintext plaintext) {
if (plaintext.getAckMessage() != null) {
long expires = UnixTime.now(+plaintext.getTTL());
LOG.info("Expires at " + expires);
proofOfWorkService.doProofOfWorkWithAck(plaintext, expires);
} else {
send(plaintext.getFrom(), plaintext.getTo(), new Msg(plaintext), plaintext.getTTL());
}
}
public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload,
final long timeToLive) {
try {
final BitmessageAddress recipient = (to != null ? to : from);
long expires = UnixTime.now(+timeToLive);
LOG.info("Expires at " + expires);
final ObjectMessage object = new ObjectMessage.Builder()
.stream(recipient.getStream())
.expiresTime(expires)
.payload(payload)
.build();
if (object.isSigned()) {
object.sign(from.getPrivateKey());
}
if (payload instanceof Broadcast) {
((Broadcast) payload).encrypt();
} else if (payload instanceof Encrypted) {
object.encrypt(recipient.getPubkey());
}
proofOfWorkService.doProofOfWork(to, object);
} catch (IOException e) {
throw new ApplicationException(e);
}
}
public void sendPubkey(final BitmessageAddress identity, final long targetStream) {
try {
long expires = UnixTime.now(TTL.pubkey());
LOG.info("Expires at " + expires);
final ObjectMessage response = new ObjectMessage.Builder()
.stream(targetStream)
.expiresTime(expires)
.payload(identity.getPubkey())
.build();
response.sign(identity.getPrivateKey());
response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey()));
// TODO: remember that the pubkey is just about to be sent, and on which stream!
proofOfWorkService.doProofOfWork(response);
} catch (IOException e) {
throw new ApplicationException(e);
}
}
/**
* 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.
*/
public void requestPubkey(final BitmessageAddress contact) {
BitmessageAddress stored = addressRepository.getAddress(contact.getAddress());
tryToFindMatchingPubkey(contact);
if (contact.getPubkey() != null) {
if (stored != null) {
stored.setPubkey(contact.getPubkey());
addressRepository.save(stored);
} else {
addressRepository.save(contact);
}
return;
}
if (stored == null) {
addressRepository.save(contact);
}
long expires = UnixTime.now(TTL.getpubkey());
LOG.info("Expires at " + expires);
final ObjectMessage request = new ObjectMessage.Builder()
.stream(contact.getStream())
.expiresTime(expires)
.payload(new GetPubkey(contact))
.build();
proofOfWorkService.doProofOfWork(request);
}
private void tryToFindMatchingPubkey(BitmessageAddress address) {
BitmessageAddress stored = addressRepository.getAddress(address.getAddress());
if (stored != null) {
address.setAlias(stored.getAlias());
address.setSubscribed(stored.isSubscribed());
}
for (ObjectMessage object : inventory.getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) {
try {
Pubkey pubkey = (Pubkey) object.getPayload();
if (address.getVersion() == 4) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
if (object.isSignatureValid(v4Pubkey)) {
address.setPubkey(v4Pubkey);
addressRepository.save(address);
break;
} else {
LOG.info("Found pubkey for " + address + " but signature is invalid");
}
}
} else {
if (Arrays.equals(pubkey.getRipe(), address.getRipe())) {
address.setPubkey(pubkey);
addressRepository.save(address);
break;
}
}
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
}
}
public void resendUnacknowledged() {
List<Plaintext> messages = messageRepository.findMessagesToResend();
for (Plaintext message : messages) {
send(message);
messageRepository.save(message);
}
}
public long getClientNonce() {
return clientNonce;
}
public long getConnectionTTL() {
return connectionTTL;
}
public int getConnectionLimit() {
return connectionLimit;
}
public CustomCommandHandler getCustomCommandHandler() {
return customCommandHandler;
}
public interface ContextHolder {
void setContext(InternalContext context);
}
}

View File

@ -0,0 +1,125 @@
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.ports.Cryptography;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES;
import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* @author Christian Basler
*/
public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalContext.ContextHolder {
private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class);
private Cryptography cryptography;
private InternalContext ctx;
private ProofOfWorkRepository powRepo;
private MessageRepository messageRepo;
public void doMissingProofOfWork(long delayInMilliseconds) {
final List<byte[]> items = powRepo.getItems();
if (items.isEmpty()) return;
// Wait for 30 seconds, to let the application start up before putting heavy load on the CPU
new Timer().schedule(new TimerTask() {
@Override
public void run() {
LOG.info("Doing POW for " + items.size() + " tasks.");
for (byte[] initialHash : items) {
Item item = powRepo.getItem(initialHash);
cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes,
ProofOfWorkService.this);
}
}
}, delayInMilliseconds);
}
public void doProofOfWork(ObjectMessage object) {
doProofOfWork(null, object);
}
public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) {
Pubkey pubkey = recipient == null ? null : recipient.getPubkey();
long nonceTrialsPerByte = pubkey == null ? NETWORK_NONCE_TRIALS_PER_BYTE : pubkey.getNonceTrialsPerByte();
long extraBytes = pubkey == null ? NETWORK_EXTRA_BYTES : pubkey.getExtraBytes();
powRepo.putObject(object, nonceTrialsPerByte, extraBytes);
if (object.getPayload() instanceof PlaintextHolder) {
Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext();
plaintext.setInitialHash(cryptography.getInitialHash(object));
messageRepo.save(plaintext);
}
cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this);
}
public void doProofOfWorkWithAck(Plaintext plaintext, long expirationTime) {
final ObjectMessage ack = plaintext.getAckMessage();
messageRepo.save(plaintext);
Item item = new 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
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
Item item = powRepo.getItem(initialHash);
if (item.message == null) {
ObjectMessage object = powRepo.getItem(initialHash).object;
object.setNonce(nonce);
Plaintext plaintext = messageRepo.getMessage(initialHash);
if (plaintext != null) {
plaintext.setInventoryVector(object.getInventoryVector());
plaintext.updateNextTry();
ctx.getLabeler().markAsSent(plaintext);
messageRepo.save(plaintext);
}
try {
ctx.getNetworkListener().receive(object);
} catch (IOException e) {
LOG.debug(e.getMessage(), e);
}
ctx.getInventory().storeObject(object);
ctx.getNetworkHandler().offer(object.getInventoryVector());
} else {
item.message.getAckMessage().setNonce(nonce);
final ObjectMessage object = new ObjectMessage.Builder()
.stream(item.message.getStream())
.expiresTime(item.expirationTime)
.payload(new Msg(item.message))
.build();
if (object.isSigned()) {
object.sign(item.message.getFrom().getPrivateKey());
}
if (object.getPayload() instanceof Encrypted) {
object.encrypt(item.message.getTo().getPubkey());
}
doProofOfWork(item.message.getTo(), object);
}
powRepo.removeObject(initialHash);
}
@Override
public void setContext(InternalContext ctx) {
this.ctx = ctx;
this.cryptography = cryptography();
this.powRepo = ctx.getProofOfWorkRepository();
this.messageRepo = ctx.getMessageRepository();
}
}

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Encode;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -29,6 +30,8 @@ import java.util.List;
* The 'addr' command holds a list of known active Bitmessage nodes.
*/
public class Addr implements MessagePayload {
private static final long serialVersionUID = -5117688017050138720L;
private final List<NetworkAddress> addresses;
private Addr(Builder builder) {
@ -45,18 +48,23 @@ public class Addr implements MessagePayload {
}
@Override
public void write(OutputStream stream) throws IOException {
Encode.varInt(addresses.size(), stream);
public void write(OutputStream out) throws IOException {
Encode.varInt(addresses.size(), out);
for (NetworkAddress address : addresses) {
address.write(stream);
address.write(out);
}
}
@Override
public void write(ByteBuffer buffer) {
Encode.varInt(addresses.size(), buffer);
for (NetworkAddress address : addresses) {
address.write(buffer);
}
}
public static final class Builder {
private List<NetworkAddress> addresses = new ArrayList<NetworkAddress>();
public Builder() {
}
private List<NetworkAddress> addresses = new ArrayList<>();
public Builder addresses(Collection<NetworkAddress> addresses){
this.addresses.addAll(addresses);

View File

@ -17,24 +17,35 @@
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.*;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Base58;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Encode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import static ch.dissem.bitmessage.utils.Decode.bytes;
import static ch.dissem.bitmessage.utils.Decode.varInt;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address
* holding private keys.
*/
public class BitmessageAddress {
public class BitmessageAddress implements Serializable {
private static final long serialVersionUID = 2386328540805994064L;
private final long version;
private final long stream;
private final byte[] ripe;
@ -51,6 +62,7 @@ public class BitmessageAddress {
private String alias;
private boolean subscribed;
private boolean chan;
BitmessageAddress(long version, long stream, byte[] ripe) {
try {
@ -62,31 +74,63 @@ public class BitmessageAddress {
Encode.varInt(version, os);
Encode.varInt(stream, os);
if (version < 4) {
byte[] checksum = Security.sha512(os.toByteArray(), ripe);
byte[] 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
byte[] checksum = Security.doubleSha512(os.toByteArray(), ripe);
byte[] 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
int offset = Bytes.numberOfLeadingZeros(ripe);
os.write(ripe, offset, ripe.length - offset);
byte[] checksum = Security.doubleSha512(os.toByteArray());
byte[] checksum = cryptography().doubleSha512(os.toByteArray());
os.write(checksum, 0, 4);
this.address = "BM-" + Base58.encode(os.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
BitmessageAddress(Pubkey publicKey) {
public BitmessageAddress(Pubkey publicKey) {
this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe());
this.pubkey = publicKey;
}
public BitmessageAddress(String address, String passphrase) {
this(address);
this.privateKey = new PrivateKey(this, passphrase);
this.pubkey = this.privateKey.getPubkey();
if (!Arrays.equals(ripe, privateKey.getPubkey().getRipe())) {
throw new IllegalArgumentException("Wrong address or passphrase");
}
}
public static BitmessageAddress chan(String address, String passphrase) {
BitmessageAddress result = new BitmessageAddress(address, passphrase);
result.chan = true;
return result;
}
public static BitmessageAddress chan(long stream, String passphrase) {
PrivateKey privateKey = new PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase);
BitmessageAddress result = new BitmessageAddress(privateKey);
result.chan = true;
return result;
}
public static List<BitmessageAddress> deterministic(String passphrase, int numberOfAddresses,
long version, long stream, boolean shorter) {
List<BitmessageAddress> result = new ArrayList<>(numberOfAddresses);
List<PrivateKey> privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter);
for (PrivateKey pk : privateKeys) {
result.add(new BitmessageAddress(pk));
}
return result;
}
public BitmessageAddress(PrivateKey privateKey) {
this(privateKey.getPubkey());
this.privateKey = privateKey;
@ -103,23 +147,23 @@ public class BitmessageAddress {
this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20);
// test checksum
byte[] checksum = Security.doubleSha512(bytes, bytes.length - 4);
byte[] checksum = cryptography().doubleSha512(bytes, bytes.length - 4);
byte[] expectedChecksum = bytes(in, 4);
for (int i = 0; i < 4; i++) {
if (expectedChecksum[i] != checksum[i])
throw new IllegalArgumentException("Checksum of address failed");
}
if (version < 4) {
checksum = Security.sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
checksum = cryptography().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
this.tag = null;
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
} else {
checksum = Security.doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
checksum = cryptography().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe);
this.tag = Arrays.copyOfRange(checksum, 32, 64);
this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32);
}
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
@ -129,9 +173,9 @@ public class BitmessageAddress {
Encode.varInt(version, out);
Encode.varInt(stream, out);
out.write(ripe);
return Arrays.copyOfRange(Security.doubleSha512(out.toByteArray()), 32, 64);
return Arrays.copyOfRange(cryptography().doubleSha512(out.toByteArray()), 32, 64);
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
@ -182,7 +226,7 @@ public class BitmessageAddress {
@Override
public String toString() {
return alias != null ? alias : address;
return alias == null ? address : alias;
}
public byte[] getRipe() {
@ -215,4 +259,19 @@ public class BitmessageAddress {
public void setSubscribed(boolean subscribed) {
this.subscribed = subscribed;
}
public boolean isChan() {
return chan;
}
public void setChan(boolean chan) {
this.chan = chan;
}
public boolean has(Feature feature) {
if (pubkey == null || feature == null) {
return false;
}
return feature.isActive(pubkey.getBehaviorBitfield());
}
}

View File

@ -0,0 +1,111 @@
/*
* 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.entity;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Encode;
import java.io.*;
import java.nio.ByteBuffer;
import static ch.dissem.bitmessage.utils.Decode.bytes;
import static ch.dissem.bitmessage.utils.Decode.varString;
/**
* @author Christian Basler
*/
public class CustomMessage implements MessagePayload {
private static final long serialVersionUID = -8932056829480326011L;
public static final String COMMAND_ERROR = "ERROR";
private final String command;
private final byte[] data;
public CustomMessage(String command) {
this.command = command;
this.data = null;
}
public CustomMessage(String command, byte[] data) {
this.command = command;
this.data = data;
}
public static CustomMessage read(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter();
return new CustomMessage(varString(in, counter), bytes(in, length - counter.length()));
}
@Override
public Command getCommand() {
return Command.CUSTOM;
}
public String getCustomCommand() {
return command;
}
public byte[] getData() {
if (data != null) {
return data;
} else {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
write(out);
return out.toByteArray();
} catch (IOException e) {
throw new ApplicationException(e);
}
}
}
@Override
public void write(OutputStream out) throws IOException {
if (data != null) {
Encode.varString(command, out);
out.write(data);
} else {
throw new ApplicationException("Tried to write custom message without data. " +
"Programmer: did you forget to override #write()?");
}
}
@Override
public void write(ByteBuffer buffer) {
if (data != null) {
Encode.varString(command, buffer);
buffer.put(data);
} else {
throw new ApplicationException("Tried to write custom message without data. " +
"Programmer: did you forget to override #write()?");
}
}
public boolean isError() {
return COMMAND_ERROR.equals(command);
}
public static CustomMessage error(String message) {
try {
return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
}
}

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Encode;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
@ -28,6 +29,10 @@ import java.util.List;
* The 'getdata' command is used to request objects from a node.
*/
public class GetData implements MessagePayload {
private static final long serialVersionUID = 1433878785969631061L;
public static final int MAX_INVENTORY_SIZE = 50_000;
List<InventoryVector> inventory;
private GetData(Builder builder) {
@ -44,19 +49,24 @@ public class GetData implements MessagePayload {
}
@Override
public void write(OutputStream stream) throws IOException {
Encode.varInt(inventory.size(), stream);
public void write(OutputStream out) throws IOException {
Encode.varInt(inventory.size(), out);
for (InventoryVector iv : inventory) {
iv.write(stream);
iv.write(out);
}
}
@Override
public void write(ByteBuffer buffer) {
Encode.varInt(inventory.size(), buffer);
for (InventoryVector iv : inventory) {
iv.write(buffer);
}
}
public static final class Builder {
private List<InventoryVector> inventory = new LinkedList<>();
public Builder() {
}
public Builder addInventoryVector(InventoryVector inventoryVector) {
this.inventory.add(inventoryVector);
return this;

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Encode;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
@ -28,6 +29,8 @@ import java.util.List;
* The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items.
*/
public class Inv implements MessagePayload {
private static final long serialVersionUID = 3662992522956947145L;
private List<InventoryVector> inventory;
private Inv(Builder builder) {
@ -44,19 +47,24 @@ public class Inv implements MessagePayload {
}
@Override
public void write(OutputStream stream) throws IOException {
Encode.varInt(inventory.size(), stream);
public void write(OutputStream out) throws IOException {
Encode.varInt(inventory.size(), out);
for (InventoryVector iv : inventory) {
iv.write(stream);
iv.write(out);
}
}
@Override
public void write(ByteBuffer buffer) {
Encode.varInt(inventory.size(), buffer);
for (InventoryVector iv : inventory) {
iv.write(buffer);
}
}
public static final class Builder {
private List<InventoryVector> inventory = new LinkedList<>();
public Builder() {
}
public Builder addInventoryVector(InventoryVector inventoryVector) {
this.inventory.add(inventoryVector);
return this;

View File

@ -23,6 +23,6 @@ public interface MessagePayload extends Streamable {
Command getCommand();
enum Command {
VERSION, VERACK, ADDR, INV, GETDATA, OBJECT
VERSION, VERACK, ADDR, INV, GETDATA, OBJECT, CUSTOM
}
}

View File

@ -16,22 +16,25 @@
package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Encode;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import static ch.dissem.bitmessage.utils.Security.sha512;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* A network message is exchanged between two nodes.
*/
public class NetworkMessage implements Streamable {
private static final long serialVersionUID = 702708857104464809L;
/**
* Magic value indicating message origin network, and used to seek to next message when stream state is unknown
*/
@ -48,7 +51,7 @@ public class NetworkMessage implements Streamable {
* First 4 bytes of sha512(payload)
*/
private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException {
byte[] d = sha512(bytes);
byte[] d = cryptography().sha512(bytes);
return new byte[]{d[0], d[1], d[2], d[3]};
}
@ -71,9 +74,7 @@ public class NetworkMessage implements Streamable {
out.write('\0');
}
ByteArrayOutputStream payloadStream = new ByteArrayOutputStream();
payload.write(payloadStream);
byte[] payloadBytes = payloadStream.toByteArray();
byte[] payloadBytes = Encode.bytes(payload);
// Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would
// ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are
@ -84,10 +85,67 @@ public class NetworkMessage implements Streamable {
try {
out.write(getChecksum(payloadBytes));
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
// 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.
*/
public ByteBuffer writeHeaderAndGetPayloadBuffer(ByteBuffer headerBuffer) {
return ByteBuffer.wrap(writeHeader(headerBuffer));
}
/**
* For improved memory efficiency, you should use {@link #writeHeaderAndGetPayloadBuffer(ByteBuffer)}
* 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
public void write(ByteBuffer buffer) {
byte[] payloadBytes = writeHeader(buffer);
buffer.put(payloadBytes);
}
private byte[] writeHeader(ByteBuffer out) {
// magic
Encode.int32(MAGIC, out);
// ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected)
String command = payload.getCommand().name().toLowerCase();
try {
out.put(command.getBytes("ASCII"));
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
for (int i = command.length(); i < 12; i++) {
out.put((byte) 0);
}
byte[] payloadBytes = Encode.bytes(payload);
// Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would
// ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are
// larger than this.
Encode.int32(payloadBytes.length, out);
// checksum
try {
out.put(getChecksum(payloadBytes));
} catch (GeneralSecurityException e) {
throw new ApplicationException(e);
}
// message payload
return payloadBytes;
}
}

View File

@ -21,19 +21,26 @@ 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.Security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* The 'object' command sends an object that is shared throughout the network.
*/
public class ObjectMessage implements MessagePayload {
private static final long serialVersionUID = 2495752480120659139L;
private byte[] nonce;
private long expiresTime;
private long objectType;
@ -51,7 +58,7 @@ public class ObjectMessage implements MessagePayload {
expiresTime = builder.expiresTime;
objectType = builder.objectType;
version = builder.payload.getVersion();
stream = builder.streamNumber;
stream = builder.streamNumber > 0 ? builder.streamNumber : builder.payload.getStream();
payload = builder.payload;
}
@ -89,7 +96,9 @@ public class ObjectMessage implements MessagePayload {
}
public InventoryVector getInventoryVector() {
return new InventoryVector(Bytes.truncate(Security.doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32));
return new InventoryVector(
Bytes.truncate(cryptography().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32)
);
}
private boolean isEncrypted() {
@ -107,13 +116,13 @@ public class ObjectMessage implements MessagePayload {
payload.writeBytesToSign(out);
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
public void sign(PrivateKey key) {
if (payload.isSigned()) {
payload.setSignature(Security.getSignature(getBytesToSign(), key));
payload.setSignature(cryptography().getSignature(getBytesToSign(), key));
}
}
@ -141,21 +150,35 @@ public class ObjectMessage implements MessagePayload {
((Encrypted) payload).encrypt(publicKey.getEncryptionKey());
}
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
public boolean isSignatureValid(Pubkey pubkey) throws IOException {
if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first");
return Security.isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey);
return cryptography().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey);
}
@Override
public void write(OutputStream out) throws IOException {
out.write(nonce);
if (nonce == null) {
out.write(new byte[8]);
} else {
out.write(nonce);
}
out.write(getPayloadBytesWithoutNonce());
}
@Override
public void write(ByteBuffer buffer) {
if (nonce == null) {
buffer.put(new byte[8]);
} else {
buffer.put(nonce);
}
buffer.put(getPayloadBytesWithoutNonce());
}
private void writeHeaderWithoutNonce(OutputStream out) throws IOException {
Encode.int64(expiresTime, out);
Encode.int32(objectType, out);
@ -173,7 +196,7 @@ public class ObjectMessage implements MessagePayload {
}
return payloadBytes;
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
@ -184,9 +207,6 @@ public class ObjectMessage implements MessagePayload {
private long streamNumber;
private ObjectPayload payload;
public Builder() {
}
public Builder nonce(byte[] nonce) {
this.nonce = nonce;
return this;
@ -223,4 +243,29 @@ public class ObjectMessage implements MessagePayload {
return new ObjectMessage(this);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ObjectMessage that = (ObjectMessage) o;
return expiresTime == that.expiresTime &&
objectType == that.objectType &&
version == that.version &&
stream == that.stream &&
Objects.equals(payload, that.payload);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(nonce);
result = 31 * result + (int) (expiresTime ^ (expiresTime >>> 32));
result = 31 * result + (int) (objectType ^ (objectType >>> 32));
result = 31 * result + (int) (version ^ (version >>> 32));
result = 31 * result + (int) (stream ^ (stream >>> 32));
result = 31 * result + (payload != null ? payload.hashCode() : 0);
return result;
}
}

View File

@ -16,24 +16,33 @@
package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.Collections;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* The unencrypted message to be sent by 'msg' or 'broadcast'.
*/
public class Plaintext implements Streamable {
private static final long serialVersionUID = -5325729856394951079L;
private final Type type;
private final BitmessageAddress from;
private final long encoding;
private final byte[] message;
private final byte[] ack;
private final byte[] ackData;
private ObjectMessage ackMessage;
private Object id;
private InventoryVector inventoryVector;
private BitmessageAddress to;
@ -43,6 +52,11 @@ public class Plaintext implements Streamable {
private Long received;
private Set<Label> labels;
private byte[] initialHash;
private long ttl;
private int retries;
private Long nextTry;
private Plaintext(Builder builder) {
id = builder.id;
@ -52,34 +66,44 @@ public class Plaintext implements Streamable {
to = builder.to;
encoding = builder.encoding;
message = builder.message;
ack = builder.ack;
ackData = builder.ackData;
if (builder.ackMessage != null && builder.ackMessage.length > 0) {
ackMessage = Factory.getObjectMessage(
3,
new ByteArrayInputStream(builder.ackMessage),
builder.ackMessage.length);
}
signature = builder.signature;
status = builder.status;
sent = builder.sent;
received = builder.received;
labels = builder.labels;
ttl = builder.ttl;
retries = builder.retries;
nextTry = builder.nextTry;
}
public static Plaintext read(Type type, InputStream in) throws IOException {
return readWithoutSignature(type, in)
.signature(Decode.varBytes(in))
.build();
.signature(Decode.varBytes(in))
.received(UnixTime.now())
.build();
}
public static Plaintext.Builder readWithoutSignature(Type type, InputStream in) throws IOException {
long version = Decode.varInt(in);
return new Builder(type)
.addressVersion(version)
.stream(Decode.varInt(in))
.behaviorBitfield(Decode.int32(in))
.publicSigningKey(Decode.bytes(in, 64))
.publicEncryptionKey(Decode.bytes(in, 64))
.nonceTrialsPerByte(version >= 3 ? Decode.varInt(in) : 0)
.extraBytes(version >= 3 ? Decode.varInt(in) : 0)
.destinationRipe(type == Type.MSG ? Decode.bytes(in, 20) : null)
.encoding(Decode.varInt(in))
.message(Decode.varBytes(in))
.ack(type == Type.MSG ? Decode.varBytes(in) : null);
.addressVersion(version)
.stream(Decode.varInt(in))
.behaviorBitfield(Decode.int32(in))
.publicSigningKey(Decode.bytes(in, 64))
.publicEncryptionKey(Decode.bytes(in, 64))
.nonceTrialsPerByte(version >= 3 ? Decode.varInt(in) : 0)
.extraBytes(version >= 3 ? Decode.varInt(in) : 0)
.destinationRipe(type == Type.MSG ? Decode.bytes(in, 20) : null)
.encoding(Decode.varInt(in))
.message(Decode.varBytes(in))
.ackMessage(type == Type.MSG ? Decode.varBytes(in) : null);
}
public InventoryVector getInventoryVector() {
@ -108,9 +132,9 @@ public class Plaintext implements Streamable {
public void setTo(BitmessageAddress to) {
if (this.to.getVersion() != 0)
throw new RuntimeException("Correct address already set");
throw new IllegalStateException("Correct address already set");
if (!Arrays.equals(this.to.getRipe(), to.getRipe())) {
throw new RuntimeException("RIPEs don't match");
throw new IllegalArgumentException("RIPEs don't match");
}
this.to = to;
}
@ -131,6 +155,15 @@ public class Plaintext implements Streamable {
this.signature = signature;
}
public boolean isUnread() {
for (Label label : labels) {
if (label.getType() == Label.Type.UNREAD) {
return true;
}
}
return false;
}
public void write(OutputStream out, boolean includeSignature) throws IOException {
Encode.varInt(from.getVersion(), out);
Encode.varInt(from.getStream(), out);
@ -148,8 +181,13 @@ public class Plaintext implements Streamable {
Encode.varInt(message.length, out);
out.write(message);
if (type == Type.MSG) {
Encode.varInt(ack.length, out);
out.write(ack);
if (to.has(Feature.DOES_ACK) && getAckMessage() != null) {
ByteArrayOutputStream ack = new ByteArrayOutputStream();
getAckMessage().write(ack);
Encode.varBytes(ack.toByteArray(), out);
} else {
Encode.varInt(0, out);
}
}
if (includeSignature) {
if (signature == null) {
@ -161,11 +199,49 @@ public class Plaintext implements Streamable {
}
}
public void write(ByteBuffer buffer, boolean includeSignature) {
Encode.varInt(from.getVersion(), buffer);
Encode.varInt(from.getStream(), buffer);
Encode.int32(from.getPubkey().getBehaviorBitfield(), buffer);
buffer.put(from.getPubkey().getSigningKey(), 1, 64);
buffer.put(from.getPubkey().getEncryptionKey(), 1, 64);
if (from.getVersion() >= 3) {
Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), buffer);
Encode.varInt(from.getPubkey().getExtraBytes(), buffer);
}
if (type == Type.MSG) {
buffer.put(to.getRipe());
}
Encode.varInt(encoding, buffer);
Encode.varInt(message.length, buffer);
buffer.put(message);
if (type == Type.MSG) {
if (to.has(Feature.DOES_ACK) && getAckMessage() != null) {
Encode.varBytes(Encode.bytes(getAckMessage()), buffer);
} else {
Encode.varInt(0, buffer);
}
}
if (includeSignature) {
if (signature == null) {
Encode.varInt(0, buffer);
} else {
Encode.varInt(signature.length, buffer);
buffer.put(signature);
}
}
}
@Override
public void write(OutputStream out) throws IOException {
write(out, true);
}
@Override
public void write(ByteBuffer buffer) {
write(buffer, true);
}
public Object getId() {
return id;
}
@ -188,9 +264,38 @@ public class Plaintext implements Streamable {
}
public void setStatus(Status status) {
if (status != Status.RECEIVED && sent == null && status != Status.DRAFT) {
sent = UnixTime.now();
}
this.status = status;
}
public long getTTL() {
return ttl;
}
public int getRetries() {
return retries;
}
public Long getNextTry() {
return nextTry;
}
public void updateNextTry() {
if (to != null) {
if (nextTry == null) {
if (sent != null && to.has(Feature.DOES_ACK)) {
nextTry = UnixTime.now(+ttl);
retries++;
}
} else {
nextTry = nextTry + (1 << retries) * ttl;
retries++;
}
}
}
public String getSubject() {
Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8");
String firstLine = s.nextLine();
@ -211,7 +316,7 @@ public class Plaintext implements Streamable {
}
return text;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
@ -221,20 +326,20 @@ public class Plaintext implements Streamable {
if (o == null || getClass() != o.getClass()) return false;
Plaintext plaintext = (Plaintext) o;
return Objects.equals(encoding, plaintext.encoding) &&
Objects.equals(from, plaintext.from) &&
Arrays.equals(message, plaintext.message) &&
Arrays.equals(ack, plaintext.ack) &&
Arrays.equals(to.getRipe(), plaintext.to.getRipe()) &&
Arrays.equals(signature, plaintext.signature) &&
Objects.equals(status, plaintext.status) &&
Objects.equals(sent, plaintext.sent) &&
Objects.equals(received, plaintext.received) &&
Objects.equals(labels, plaintext.labels);
Objects.equals(from, plaintext.from) &&
Arrays.equals(message, plaintext.message) &&
Objects.equals(getAckMessage(), plaintext.getAckMessage()) &&
Arrays.equals(to == null ? null : to.getRipe(), plaintext.to == null ? null : plaintext.to.getRipe()) &&
Arrays.equals(signature, plaintext.signature) &&
Objects.equals(status, plaintext.status) &&
Objects.equals(sent, plaintext.sent) &&
Objects.equals(received, plaintext.received) &&
Objects.equals(labels, plaintext.labels);
}
@Override
public int hashCode() {
return Objects.hash(from, encoding, message, ack, to, signature, status, sent, received, labels);
return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels);
}
public void addLabels(Label... labels) {
@ -245,10 +350,41 @@ public class Plaintext implements Streamable {
public void addLabels(Collection<Label> labels) {
if (labels != null) {
this.labels.addAll(labels);
for (Label label : labels) {
this.labels.add(label);
}
}
}
public void removeLabel(Label.Type type) {
Iterator<Label> iterator = labels.iterator();
while (iterator.hasNext()) {
Label label = iterator.next();
if (label.getType() == type) {
iterator.remove();
}
}
}
public byte[] getAckData() {
return ackData;
}
public ObjectMessage getAckMessage() {
if (ackMessage == null) {
ackMessage = Factory.createAck(this);
}
return ackMessage;
}
public void setInitialHash(byte[] initialHash) {
this.initialHash = initialHash;
}
public byte[] getInitialHash() {
return initialHash;
}
public enum Encoding {
IGNORE(0), TRIVIAL(1), SIMPLE(2);
@ -293,12 +429,16 @@ public class Plaintext implements Streamable {
private byte[] destinationRipe;
private long encoding;
private byte[] message = new byte[0];
private byte[] ack = new byte[0];
private byte[] ackData;
private byte[] ackMessage;
private byte[] signature;
private long sent;
private long received;
private Status status;
private Set<Label> labels = new HashSet<>();
private long ttl;
private int retries;
private Long nextTry;
public Builder(Type type) {
this.type = type;
@ -382,7 +522,7 @@ public class Plaintext implements Streamable {
this.encoding = Encoding.SIMPLE.getCode();
this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
return this;
}
@ -392,9 +532,16 @@ public class Plaintext implements Streamable {
return this;
}
public Builder ack(byte[] ack) {
if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ack only allowed for msg");
this.ack = ack;
public Builder ackMessage(byte[] ack) {
if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ackMessage only allowed for msg");
this.ackMessage = ack;
return this;
}
public Builder ackData(byte[] ackData) {
if (type != Type.MSG && ackData != null)
throw new IllegalArgumentException("ackMessage only allowed for msg");
this.ackData = ackData;
return this;
}
@ -423,21 +570,42 @@ public class Plaintext implements Streamable {
return this;
}
public Builder ttl(long ttl) {
this.ttl = ttl;
return this;
}
public Builder retries(int retries) {
this.retries = retries;
return this;
}
public Builder nextTry(Long nextTry) {
this.nextTry = nextTry;
return this;
}
public Plaintext build() {
if (from == null) {
from = new BitmessageAddress(Factory.createPubkey(
addressVersion,
stream,
publicSigningKey,
publicEncryptionKey,
nonceTrialsPerByte,
extraBytes,
behaviorBitfield
addressVersion,
stream,
publicSigningKey,
publicEncryptionKey,
nonceTrialsPerByte,
extraBytes,
behaviorBitfield
));
}
if (to == null && type != Type.BROADCAST) {
if (to == null && type != Type.BROADCAST && destinationRipe != null) {
to = new BitmessageAddress(0, 0, destinationRipe);
}
if (type == Type.MSG && ackMessage == null && ackData == null) {
ackData = cryptography().randomBytes(Msg.ACK_LENGTH);
}
if (ttl <= 0) {
ttl = TTL.msg();
}
return new Plaintext(this);
}
}

View File

@ -18,10 +18,14 @@ package ch.dissem.bitmessage.entity;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
/**
* An object that can be written to an {@link OutputStream}
*/
public interface Streamable {
public interface Streamable extends Serializable {
void write(OutputStream stream) throws IOException;
void write(ByteBuffer buffer);
}

View File

@ -18,11 +18,14 @@ package ch.dissem.bitmessage.entity;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* The 'verack' command answers a 'version' command, accepting the other node's version.
*/
public class VerAck implements MessagePayload {
private static final long serialVersionUID = -4302074845199181687L;
@Override
public Command getCommand() {
return Command.VERACK;
@ -32,4 +35,9 @@ public class VerAck implements MessagePayload {
public void write(OutputStream stream) throws IOException {
// 'verack' doesn't have any payload, so there is nothing to write
}
@Override
public void write(ByteBuffer buffer) {
// 'verack' doesn't have any payload, so there is nothing to write
}
}

View File

@ -23,12 +23,14 @@ import ch.dissem.bitmessage.utils.UnixTime;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
import java.nio.ByteBuffer;
/**
* The 'version' command advertises this node's latest supported protocol version upon initiation.
*/
public class Version implements MessagePayload {
private static final long serialVersionUID = 7219240857343176567L;
/**
* 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.
@ -132,6 +134,18 @@ public class Version implements MessagePayload {
Encode.varIntList(streams, stream);
}
@Override
public void write(ByteBuffer buffer) {
Encode.int32(version, buffer);
Encode.int64(services, buffer);
Encode.int64(timestamp, buffer);
addrRecv.write(buffer, true);
addrFrom.write(buffer, true);
Encode.int64(nonce, buffer);
Encode.varString(userAgent, buffer);
Encode.varIntList(streams, buffer);
}
public static final class Builder {
private int version;
@ -143,16 +157,13 @@ public class Version implements MessagePayload {
private String userAgent;
private long[] streamNumbers;
public Builder() {
}
public Builder defaults() {
public Builder defaults(long clientNonce) {
version = BitmessageContext.CURRENT_VERSION;
services = 1;
timestamp = UnixTime.now();
nonce = new Random().nextInt();
userAgent = "/Jabit:0.0.1/";
streamNumbers = new long[]{1};
nonce = clientNonce;
return this;
}

View File

@ -21,17 +21,20 @@ import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.Security;
import java.io.IOException;
import java.util.Objects;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* Users who are subscribed to the sending address will see the message appear in their inbox.
* Broadcasts are version 4 or 5.
*/
public abstract class Broadcast extends ObjectPayload implements Encrypted, PlaintextHolder {
private static final long serialVersionUID = 4064521827582239069L;
protected final long stream;
protected CryptoBox encrypted;
protected Plaintext plaintext;
@ -78,7 +81,7 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai
}
public void encrypt() throws IOException {
encrypt(Security.createPublicKey(plaintext.getFrom().getPublicDecryptionKey()).getEncoded(false));
encrypt(cryptography().createPublicKey(plaintext.getFrom().getPublicDecryptionKey()));
}
@Override
@ -94,4 +97,18 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai
public boolean isDecrypted() {
return plaintext != null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Broadcast broadcast = (Broadcast) o;
return stream == broadcast.stream &&
(Objects.equals(encrypted, broadcast.encrypted) || Objects.equals(plaintext, broadcast.plaintext));
}
@Override
public int hashCode() {
return Objects.hash(stream);
}
}

View File

@ -17,64 +17,56 @@
package ch.dissem.bitmessage.entity.payload;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.*;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
public class CryptoBox implements Streamable {
private static final long serialVersionUID = 7217659539975573852L;
private static final Logger LOG = LoggerFactory.getLogger(CryptoBox.class);
private final byte[] initializationVector;
private final int curveType;
private final ECPoint R;
private final byte[] R;
private final byte[] mac;
private byte[] encrypted;
public CryptoBox(Streamable data, byte[] encryptionKey) throws IOException {
this(data, Security.keyToPoint(encryptionKey));
public CryptoBox(Streamable data, byte[] K) throws IOException {
this(Encode.bytes(data), K);
}
public CryptoBox(Streamable data, ECPoint K) throws IOException {
public CryptoBox(byte[] data, byte[] K) throws IOException {
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 = Security.randomBytes(16);
initializationVector = cryptography().randomBytes(16);
// 3. Generate a new random EC key pair with private key called r and public key called R.
byte[] r = Security.randomBytes(PRIVATE_KEY_SIZE);
R = Security.createPublicKey(r);
byte[] 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.
ECPoint P = K.multiply(Security.keyToBigInt(r)).normalize();
byte[] X = P.getXCoord().getEncoded();
byte[] P = cryptography().multiply(K, r);
byte[] X = Points.getX(P);
// 5. Use the X component of public key P and calculate the SHA512 hash H.
byte[] H = Security.sha512(X);
byte[] H = cryptography().sha512(X);
// 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m.
byte[] key_e = Arrays.copyOfRange(H, 0, 32);
byte[] 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 = crypt(true, Encode.bytes(data), key_e);
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);
@ -84,7 +76,7 @@ public class CryptoBox implements Streamable {
private CryptoBox(Builder builder) {
initializationVector = builder.initializationVector;
curveType = builder.curveType;
R = Security.createPoint(builder.xComponent, builder.yComponent);
R = cryptography().createPoint(builder.xComponent, builder.yComponent);
encrypted = builder.encrypted;
mac = builder.mac;
}
@ -102,18 +94,17 @@ public class CryptoBox implements Streamable {
}
/**
* @param privateKey a private key, typically should be 32 bytes long
* @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 <a href='https://bitmessage.org/wiki/Encryption#Decryption'>https://bitmessage.org/wiki/Encryption#Decryption</a>
*/
public InputStream decrypt(byte[] privateKey) throws DecryptionFailedException {
public InputStream decrypt(byte[] k) throws DecryptionFailedException {
// 1. The private key used to decrypt is called k.
BigInteger k = Security.keyToBigInt(privateKey);
// 2. Do an EC point multiply with private key k and public key R. This gives you public key P.
ECPoint P = R.multiply(k).normalize();
byte[] P = cryptography().multiply(R, k);
// 3. Use the X component of public key P and calculate the SHA512 hash H.
byte[] H = Security.sha512(P.getXCoord().getEncoded());
byte[] 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.
byte[] key_e = Arrays.copyOfRange(H, 0, 32);
byte[] key_m = Arrays.copyOfRange(H, 32, 64);
@ -126,61 +117,57 @@ public class CryptoBox implements Streamable {
// 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 new ByteArrayInputStream(crypt(false, encrypted, key_e));
return new ByteArrayInputStream(cryptography().crypt(false, encrypted, key_e, initializationVector));
}
private byte[] calculateMac(byte[] key_m) {
try {
ByteArrayOutputStream macData = new ByteArrayOutputStream();
writeWithoutMAC(macData);
return Security.mac(key_m, macData.toByteArray());
return cryptography().mac(key_m, macData.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
private byte[] crypt(boolean encrypt, byte[] data, byte[] key_e) {
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding());
CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector);
cipher.init(encrypt, params);
byte[] buffer = new byte[cipher.getOutputSize(data.length)];
int length = cipher.processBytes(data, 0, data.length, buffer, 0);
try {
length += cipher.doFinal(buffer, length);
} catch (InvalidCipherTextException e) {
throw new IllegalArgumentException(e);
}
if (length < buffer.length) {
return Arrays.copyOfRange(buffer, 0, length);
}
return buffer;
}
private void writeWithoutMAC(OutputStream out) throws IOException {
out.write(initializationVector);
Encode.int16(curveType, out);
writeCoordinateComponent(out, R.getXCoord());
writeCoordinateComponent(out, R.getYCoord());
writeCoordinateComponent(out, Points.getX(R));
writeCoordinateComponent(out, Points.getY(R));
out.write(encrypted);
}
private void writeCoordinateComponent(OutputStream out, ECFieldElement coord) throws IOException {
byte[] x = coord.getEncoded();
private void writeCoordinateComponent(OutputStream out, byte[] x) throws IOException {
int offset = Bytes.numberOfLeadingZeros(x);
int length = x.length - offset;
Encode.int16(length, out);
out.write(x, offset, length);
}
private void writeCoordinateComponent(ByteBuffer buffer, byte[] x) {
int offset = Bytes.numberOfLeadingZeros(x);
int length = x.length - offset;
Encode.int16(length, buffer);
buffer.put(x, offset, length);
}
@Override
public void write(OutputStream stream) throws IOException {
writeWithoutMAC(stream);
stream.write(mac);
}
@Override
public void write(ByteBuffer buffer) {
buffer.put(initializationVector);
Encode.int16(curveType, buffer);
writeCoordinateComponent(buffer, Points.getX(R));
writeCoordinateComponent(buffer, Points.getY(R));
buffer.put(encrypted);
buffer.put(mac);
}
public static final class Builder {
private byte[] initializationVector;
private int curveType;
@ -195,7 +182,7 @@ public class CryptoBox implements Streamable {
}
public Builder curveType(int curveType) {
if (curveType != 0x2CA) LOG.debug("Unexpected curve type " + curveType);
if (curveType != 0x2CA) LOG.trace("Unexpected curve type " + curveType);
this.curveType = curveType;
return this;
}

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Decode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
@ -28,6 +29,8 @@ import java.util.Arrays;
* have to know what it is.
*/
public class GenericPayload extends ObjectPayload {
private static final long serialVersionUID = -912314085064185940L;
private long stream;
private byte[] data;
@ -37,7 +40,7 @@ public class GenericPayload extends ObjectPayload {
this.data = data;
}
public static GenericPayload read(long version, InputStream is, long stream, int length) throws IOException {
public static GenericPayload read(long version, long stream, InputStream is, int length) throws IOException {
return new GenericPayload(version, stream, Decode.bytes(is, length));
}
@ -51,11 +54,20 @@ public class GenericPayload extends ObjectPayload {
return stream;
}
public byte[] getData() {
return data;
}
@Override
public void write(OutputStream stream) throws IOException {
stream.write(data);
}
@Override
public void write(ByteBuffer buffer) {
buffer.put(data);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -22,11 +22,14 @@ import ch.dissem.bitmessage.utils.Decode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* Request for a public key.
*/
public class GetPubkey extends ObjectPayload {
private static final long serialVersionUID = -3634516646972610180L;
private long stream;
private byte[] ripeTag;
@ -71,4 +74,9 @@ public class GetPubkey extends ObjectPayload {
public void write(OutputStream stream) throws IOException {
stream.write(ripeTag);
}
@Override
public void write(ByteBuffer buffer) {
buffer.put(ripeTag);
}
}

View File

@ -24,6 +24,8 @@ import ch.dissem.bitmessage.exception.DecryptionFailedException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Objects;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
@ -31,6 +33,9 @@ import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
* Used for person-to-person messages.
*/
public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder {
private static final long serialVersionUID = 4327495048296365733L;
public static final int ACK_LENGTH = 32;
private long stream;
private CryptoBox encrypted;
private Plaintext plaintext;
@ -106,4 +111,25 @@ public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder {
if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it.");
encrypted.write(out);
}
@Override
public void write(ByteBuffer buffer) {
if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it.");
encrypted.write(buffer);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Msg msg = (Msg) o;
return stream == msg.stream &&
(Objects.equals(encrypted, msg.encrypted) || Objects.equals(plaintext, msg.plaintext));
}
@Override
public int hashCode() {
return (int) stream;
}
}

View File

@ -26,6 +26,8 @@ import java.io.OutputStream;
* The payload of an 'object' command. This is shared by the network.
*/
public abstract class ObjectPayload implements Streamable {
private static final long serialVersionUID = -5034977402902364482L;
private final long version;
protected ObjectPayload(long version) {

View File

@ -18,15 +18,17 @@ package ch.dissem.bitmessage.entity.payload;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import static ch.dissem.bitmessage.utils.Security.ripemd160;
import static ch.dissem.bitmessage.utils.Security.sha512;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* Public keys for signing and encryption, the answer to a 'getpubkey' request.
*/
public abstract class Pubkey extends ObjectPayload {
private static final long serialVersionUID = -6634533361454999619L;
public final static long LATEST_VERSION = 4;
protected Pubkey(long version) {
@ -34,7 +36,7 @@ public abstract class Pubkey extends ObjectPayload {
}
public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) {
return ripemd160(sha512(publicSigningKey, publicEncryptionKey));
return cryptography().ripemd160(cryptography().sha512(publicSigningKey, publicEncryptionKey));
}
public abstract byte[] getSigningKey();
@ -44,7 +46,7 @@ public abstract class Pubkey extends ObjectPayload {
public abstract int getBehaviorBitfield();
public byte[] getRipe() {
return ripemd160(sha512(getSigningKey(), getEncryptionKey()));
return cryptography().ripemd160(cryptography().sha512(getSigningKey(), getEncryptionKey()));
}
public long getNonceTrialsPerByte() {
@ -59,6 +61,10 @@ public abstract class Pubkey extends ObjectPayload {
write(out);
}
public void writeUnencrypted(ByteBuffer buffer){
write(buffer);
}
protected byte[] add0x04(byte[] key) {
if (key.length == 65) return key;
byte[] result = new byte[65];
@ -75,16 +81,19 @@ public abstract class Pubkey extends ObjectPayload {
* Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg
* messages bound for them.
*/
INCLUDE_DESTINATION(1 << 30),
INCLUDE_DESTINATION(30),
/**
* If true, the receiving node does send acknowledgements (rather than dropping them).
*/
DOES_ACK(1 << 31);
DOES_ACK(31);
private int bit;
Feature(int bit) {
this.bit = bit;
Feature(int bitNumber) {
// 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
this.bit = 1 << (31 - bitNumber);
}
public static int bitfield(Feature... features) {
@ -104,5 +113,9 @@ public abstract class Pubkey extends ObjectPayload {
}
return features.toArray(new Feature[features.size()]);
}
public boolean isActive(int bitfield) {
return (bitfield & bit) != 0;
}
}
}

View File

@ -22,11 +22,14 @@ import ch.dissem.bitmessage.utils.Encode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* A version 2 public key.
*/
public class V2Pubkey extends Pubkey {
private static final long serialVersionUID = -257598690676510460L;
protected long stream;
protected int behaviorBitfield;
protected byte[] publicSigningKey; // 64 Bytes
@ -84,10 +87,17 @@ public class V2Pubkey extends Pubkey {
}
@Override
public void write(OutputStream os) throws IOException {
Encode.int32(behaviorBitfield, os);
os.write(publicSigningKey, 1, 64);
os.write(publicEncryptionKey, 1, 64);
public void write(OutputStream out) throws IOException {
Encode.int32(behaviorBitfield, out);
out.write(publicSigningKey, 1, 64);
out.write(publicEncryptionKey, 1, 64);
}
@Override
public void write(ByteBuffer buffer) {
Encode.int32(behaviorBitfield, buffer);
buffer.put(publicSigningKey, 1, 64);
buffer.put(publicEncryptionKey, 1, 64);
}
public static class Builder {
@ -96,9 +106,6 @@ public class V2Pubkey extends Pubkey {
private byte[] publicSigningKey;
private byte[] publicEncryptionKey;
public Builder() {
}
public Builder stream(long streamNumber) {
this.streamNumber = streamNumber;
return this;

View File

@ -29,6 +29,8 @@ import java.util.Objects;
* A version 3 public key.
*/
public class V3Pubkey extends V2Pubkey {
private static final long serialVersionUID = 6958853116648528319L;
long nonceTrialsPerByte;
long extraBytes;
byte[] signature;
@ -123,9 +125,6 @@ public class V3Pubkey extends V2Pubkey {
private long extraBytes;
private byte[] signature = new byte[0];
public Builder() {
}
public Builder stream(long streamNumber) {
this.streamNumber = streamNumber;
return this;

View File

@ -22,12 +22,15 @@ import ch.dissem.bitmessage.entity.Plaintext;
import java.io.IOException;
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.
*/
public class V4Broadcast extends Broadcast {
private static final long serialVersionUID = 195663108282762711L;
protected V4Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) {
super(version, stream, encrypted, plaintext);
}
@ -56,4 +59,9 @@ public class V4Broadcast extends Broadcast {
public void write(OutputStream out) throws IOException {
encrypted.write(out);
}
@Override
public void write(ByteBuffer buffer) {
encrypted.write(buffer);
}
}

View File

@ -24,6 +24,7 @@ import ch.dissem.bitmessage.utils.Decode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
@ -33,6 +34,8 @@ import java.util.Arrays;
* to create messages to be used in spam or in flooding attacks.
*/
public class V4Pubkey extends Pubkey implements Encrypted {
private static final long serialVersionUID = 1556710353694033093L;
private long stream;
private byte[] tag;
private CryptoBox encrypted;
@ -83,11 +86,22 @@ public class V4Pubkey extends Pubkey implements Encrypted {
encrypted.write(stream);
}
@Override
public void write(ByteBuffer buffer) {
buffer.put(tag);
encrypted.write(buffer);
}
@Override
public void writeUnencrypted(OutputStream out) throws IOException {
decrypted.write(out);
}
@Override
public void writeUnencrypted(ByteBuffer buffer) {
decrypted.write(buffer);
}
@Override
public void writeBytesToSign(OutputStream out) throws IOException {
out.write(tag);

View File

@ -28,6 +28,8 @@ import java.io.OutputStream;
* Users who are subscribed to the sending address will see the message appear in their inbox.
*/
public class V5Broadcast extends V4Broadcast {
private static final long serialVersionUID = 920649721626968644L;
private byte[] tag;
private V5Broadcast(long stream, byte[] tag, CryptoBox encrypted) {

View File

@ -21,9 +21,13 @@ import ch.dissem.bitmessage.utils.Strings;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class InventoryVector implements Streamable {
public class InventoryVector implements Streamable, Serializable {
private static final long serialVersionUID = -7349009673063348719L;
/**
* Hash of the object
*/
@ -37,12 +41,11 @@ public class InventoryVector implements Streamable {
InventoryVector that = (InventoryVector) o;
return Arrays.equals(hash, that.hash);
}
@Override
public int hashCode() {
return hash != null ? Arrays.hashCode(hash) : 0;
return hash == null ? 0 : Arrays.hashCode(hash);
}
public byte[] getHash() {
@ -54,8 +57,13 @@ public class InventoryVector implements Streamable {
}
@Override
public void write(OutputStream stream) throws IOException {
stream.write(hash);
public void write(OutputStream out) throws IOException {
out.write(hash);
}
@Override
public void write(ByteBuffer buffer) {
buffer.put(hash);
}
@Override

View File

@ -16,9 +16,12 @@
package ch.dissem.bitmessage.entity.valueobject;
import java.io.Serializable;
import java.util.Objects;
public class Label {
public class Label implements Serializable {
private static final long serialVersionUID = 831782893630994914L;
private Object id;
private String label;
private Type type;
@ -78,6 +81,7 @@ public class Label {
INBOX,
BROADCAST,
DRAFT,
OUTBOX,
SENT,
UNREAD,
TRASH

View File

@ -17,37 +17,43 @@
package ch.dissem.bitmessage.entity.valueobject;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.UnixTime;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* A node's address. It's written in IPv6 format.
*/
public class NetworkAddress implements Streamable {
private static final long serialVersionUID = 2500120578167100300L;
private long time;
/**
* Stream number for this node
*/
private long stream;
private final long stream;
/**
* same service(s) listed in version
*/
private long services;
private final long services;
/**
* 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).
*/
private byte[] ipv6;
private int port;
private final byte[] ipv6;
private final int port;
private NetworkAddress(Builder builder) {
time = builder.time;
@ -85,7 +91,7 @@ public class NetworkAddress implements Streamable {
try {
return InetAddress.getByAddress(ipv6);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
throw new ApplicationException(e);
}
}
@ -116,14 +122,29 @@ public class NetworkAddress implements Streamable {
write(stream, false);
}
public void write(OutputStream stream, boolean light) throws IOException {
public void write(OutputStream out, boolean light) throws IOException {
if (!light) {
Encode.int64(time, stream);
Encode.int32(this.stream, stream);
Encode.int64(time, out);
Encode.int32(stream, out);
}
Encode.int64(services, stream);
stream.write(ipv6);
Encode.int16(port, stream);
Encode.int64(services, out);
out.write(ipv6);
Encode.int16(port, out);
}
@Override
public void write(ByteBuffer buffer) {
write(buffer, false);
}
public void write(ByteBuffer buffer, boolean light) {
if (!light) {
Encode.int64(time, buffer);
Encode.int32(stream, buffer);
}
Encode.int64(services, buffer);
buffer.put(ipv6);
Encode.int16(port, buffer);
}
public static final class Builder {
@ -133,9 +154,6 @@ public class NetworkAddress implements Streamable {
private byte[] ipv6;
private int port;
public Builder() {
}
public Builder time(final long time) {
this.time = time;
return this;
@ -199,6 +217,17 @@ public class NetworkAddress implements Streamable {
return this;
}
public Builder address(SocketAddress address) {
if (address instanceof InetSocketAddress) {
InetSocketAddress inetAddress = (InetSocketAddress) address;
ip(inetAddress.getAddress());
port(inetAddress.getPort());
} else {
throw new IllegalArgumentException("Unknown type of address: " + address.getClass());
}
return this;
}
public NetworkAddress build() {
if (time == 0) {
time = UnixTime.now();

View File

@ -0,0 +1,198 @@
/*
* 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.entity.valueobject;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying
* {@link Pubkey} object.
*/
public class PrivateKey implements Streamable {
private static final long serialVersionUID = 8562555470709110558L;
public static final int PRIVATE_KEY_SIZE = 32;
private final byte[] privateSigningKey;
private final byte[] privateEncryptionKey;
private final Pubkey pubkey;
public PrivateKey(boolean shorter, long stream, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) {
byte[] privSK;
byte[] pubSK;
byte[] privEK;
byte[] pubEK;
byte[] ripe;
do {
privSK = cryptography().randomBytes(PRIVATE_KEY_SIZE);
privEK = cryptography().randomBytes(PRIVATE_KEY_SIZE);
pubSK = cryptography().createPublicKey(privSK);
pubEK = cryptography().createPublicKey(privEK);
ripe = Pubkey.getRipe(pubSK, pubEK);
} while (ripe[0] != 0 || (shorter && ripe[1] != 0));
this.privateSigningKey = privSK;
this.privateEncryptionKey = privEK;
this.pubkey = cryptography().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey,
nonceTrialsPerByte, extraBytes, features);
}
public PrivateKey(byte[] privateSigningKey, byte[] privateEncryptionKey, Pubkey pubkey) {
this.privateSigningKey = privateSigningKey;
this.privateEncryptionKey = privateEncryptionKey;
this.pubkey = pubkey;
}
public PrivateKey(BitmessageAddress address, String passphrase) {
this(address.getVersion(), address.getStream(), passphrase);
}
public PrivateKey(long version, long stream, String passphrase) {
this(new Builder(version, stream, false).seed(passphrase).generate());
}
private PrivateKey(Builder builder) {
this.privateSigningKey = builder.privSK;
this.privateEncryptionKey = builder.privEK;
this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK, builder.pubEK,
InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES);
}
private static class Builder {
final long version;
final long stream;
final boolean shorter;
byte[] seed;
long nextNonce;
byte[] privSK, privEK;
byte[] pubSK, pubEK;
private Builder(long version, long stream, boolean shorter) {
this.version = version;
this.stream = stream;
this.shorter = shorter;
}
Builder seed(String passphrase) {
try {
seed = passphrase.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
return this;
}
Builder generate() {
long signingKeyNonce = nextNonce;
long encryptionKeyNonce = nextNonce + 1;
byte[] ripe;
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] != 0 || (shorter && ripe[1] != 0));
nextNonce = signingKeyNonce;
return this;
}
}
public static List<PrivateKey> deterministic(String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) {
List<PrivateKey> result = new ArrayList<>(numberOfAddresses);
Builder builder = new Builder(version, stream, shorter).seed(passphrase);
for (int i = 0; i < numberOfAddresses; i++) {
builder.generate();
result.add(new PrivateKey(builder));
}
return result;
}
public static PrivateKey read(InputStream is) throws IOException {
int version = (int) Decode.varInt(is);
long stream = Decode.varInt(is);
int len = (int) Decode.varInt(is);
Pubkey pubkey = Factory.readPubkey(version, stream, is, len, false);
len = (int) Decode.varInt(is);
byte[] signingKey = Decode.bytes(is, len);
len = (int) Decode.varInt(is);
byte[] encryptionKey = Decode.bytes(is, len);
return new PrivateKey(signingKey, encryptionKey, pubkey);
}
public byte[] getPrivateSigningKey() {
return privateSigningKey;
}
public byte[] getPrivateEncryptionKey() {
return privateEncryptionKey;
}
public Pubkey getPubkey() {
return pubkey;
}
@Override
public void write(OutputStream out) throws IOException {
Encode.varInt(pubkey.getVersion(), out);
Encode.varInt(pubkey.getStream(), out);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
pubkey.writeUnencrypted(baos);
Encode.varInt(baos.size(), out);
out.write(baos.toByteArray());
Encode.varInt(privateSigningKey.length, out);
out.write(privateSigningKey);
Encode.varInt(privateEncryptionKey.length, out);
out.write(privateEncryptionKey);
}
@Override
public void write(ByteBuffer buffer) {
Encode.varInt(pubkey.getVersion(), buffer);
Encode.varInt(pubkey.getStream(), buffer);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
pubkey.writeUnencrypted(baos);
Encode.varBytes(baos.toByteArray(), buffer);
} catch (IOException e) {
throw new ApplicationException(e);
}
Encode.varBytes(privateSigningKey, buffer);
Encode.varBytes(privateEncryptionKey, buffer);
}
}

View File

@ -20,6 +20,8 @@ package ch.dissem.bitmessage.exception;
* Indicates an illegal Bitmessage address
*/
public class AddressFormatException extends RuntimeException {
private static final long serialVersionUID = 6943764578672021573L;
public AddressFormatException(String message) {
super(message);
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2016 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
*/
public class ApplicationException extends RuntimeException {
private static final long serialVersionUID = 1796776684126759324L;
public ApplicationException(Throwable cause) {
super(cause);
}
public ApplicationException(String message) {
super(message);
}
}

View File

@ -17,4 +17,5 @@
package ch.dissem.bitmessage.exception;
public class DecryptionFailedException extends Exception {
private static final long serialVersionUID = 3241116253113872731L;
}

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import java.util.Arrays;
public class InsufficientProofOfWorkException extends IOException {
private static final long serialVersionUID = 9105580366564571318L;
public InsufficientProofOfWorkException(byte[] target, byte[] hash) {
super("Insufficient proof of work: " + Strings.hex(target) + " required, " + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved.");
}

View File

@ -22,6 +22,8 @@ package ch.dissem.bitmessage.exception;
* @author Ch. Basler
*/
public class NodeException extends RuntimeException {
private static final long serialVersionUID = 2965325796118227802L;
public NodeException(String message) {
super(message);
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2016 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;
import static ch.dissem.bitmessage.ports.NetworkHandler.HEADER_SIZE;
import static ch.dissem.bitmessage.ports.NetworkHandler.MAX_PAYLOAD_SIZE;
/**
* A pool for {@link ByteBuffer}s. As they may use up a lot of memory,
* they should be reused as efficiently as possible.
*/
class BufferPool {
private static final Logger LOG = LoggerFactory.getLogger(BufferPool.class);
public static final BufferPool bufferPool = new BufferPool();
private final Map<Integer, Stack<ByteBuffer>> pools = new TreeMap<>();
private BufferPool() {
pools.put(HEADER_SIZE, new Stack<ByteBuffer>());
pools.put(54, new Stack<ByteBuffer>());
pools.put(1000, new Stack<ByteBuffer>());
pools.put(60000, new Stack<ByteBuffer>());
pools.put(MAX_PAYLOAD_SIZE, new Stack<ByteBuffer>());
}
public synchronized ByteBuffer allocate(int capacity) {
Integer targetSize = getTargetSize(capacity);
Stack<ByteBuffer> pool = pools.get(targetSize);
if (pool.isEmpty()) {
LOG.trace("Creating new buffer of size " + targetSize);
return ByteBuffer.allocate(targetSize);
} else {
return pool.pop();
}
}
/**
* Returns a buffer that has the size of the Bitmessage network message header, 24 bytes.
*
* @return a buffer of size 24
*/
public synchronized ByteBuffer allocateHeaderBuffer() {
Stack<ByteBuffer> pool = pools.get(HEADER_SIZE);
if (pool.isEmpty()) {
return ByteBuffer.allocate(HEADER_SIZE);
} else {
return pool.pop();
}
}
public synchronized void deallocate(ByteBuffer buffer) {
buffer.clear();
Stack<ByteBuffer> pool = pools.get(buffer.capacity());
if (pool == null) {
throw new IllegalArgumentException("Illegal buffer capacity " + buffer.capacity() +
" one of " + pools.keySet() + " expected.");
} else {
pool.push(buffer);
}
}
private Integer getTargetSize(int capacity) {
for (Integer size : pools.keySet()) {
if (size >= capacity) return size;
}
throw new IllegalArgumentException("Requested capacity too large: " +
"requested=" + capacity + "; max=" + MAX_PAYLOAD_SIZE);
}
}

View File

@ -23,7 +23,8 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.NodeException;
import ch.dissem.bitmessage.utils.Security;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.UnixTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,11 +33,14 @@ import java.io.InputStream;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import static ch.dissem.bitmessage.entity.payload.ObjectType.MSG;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* Creates {@link NetworkMessage} objects from {@link InputStream InputStreams}
*/
public class Factory {
public static final Logger LOG = LoggerFactory.getLogger(Factory.class);
private static final Logger LOG = LoggerFactory.getLogger(Factory.class);
public static NetworkMessage getNetworkMessage(int version, InputStream stream) throws SocketTimeoutException {
try {
@ -115,8 +119,8 @@ public class Factory {
BitmessageAddress temp = new BitmessageAddress(address);
PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey,
createPubkey(temp.getVersion(), temp.getStream(),
Security.createPublicKey(privateSigningKey).getEncoded(false),
Security.createPublicKey(privateEncryptionKey).getEncoded(false),
cryptography().createPublicKey(privateSigningKey),
cryptography().createPublicKey(privateEncryptionKey),
nonceTrialsPerByte, extraBytes, behaviourBitfield));
BitmessageAddress result = new BitmessageAddress(privateKey);
if (!result.getAddress().equals(address)) {
@ -126,11 +130,17 @@ public class Factory {
return result;
}
public static BitmessageAddress generatePrivateAddress(boolean shorter, long stream, Pubkey.Feature... features) {
public static BitmessageAddress generatePrivateAddress(boolean shorter,
long stream,
Pubkey.Feature... features) {
return new BitmessageAddress(new PrivateKey(shorter, stream, 1000, 1000, features));
}
static ObjectPayload getObjectPayload(long objectType, long version, long streamNumber, InputStream stream, int length) throws IOException {
static ObjectPayload getObjectPayload(long objectType,
long version,
long streamNumber,
InputStream stream,
int length) throws IOException {
ObjectType type = ObjectType.fromNumber(objectType);
if (type != null) {
switch (type) {
@ -147,8 +157,8 @@ public class Factory {
}
}
// fallback: just store the message - we don't really care what it is
// LOG.info("Unexpected object type: " + objectType);
return GenericPayload.read(version, stream, streamNumber, length);
LOG.trace("Unexpected object type: " + objectType);
return GenericPayload.read(version, streamNumber, stream, length);
}
private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException {
@ -170,7 +180,7 @@ public class Factory {
private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException {
Pubkey pubkey = readPubkey(version, streamNumber, stream, length, true);
return pubkey != null ? pubkey : GenericPayload.read(version, stream, streamNumber, length);
return pubkey != null ? pubkey : GenericPayload.read(version, streamNumber, stream, length);
}
private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException {
@ -185,15 +195,23 @@ public class Factory {
return V5Broadcast.read(stream, streamNumber, length);
default:
LOG.debug("Encountered unknown broadcast version " + version);
return GenericPayload.read(version, stream, streamNumber, length);
return GenericPayload.read(version, streamNumber, stream, length);
}
}
public static ObjectPayload getBroadcast(BitmessageAddress sendingAddress, Plaintext plaintext) {
public static Broadcast getBroadcast(Plaintext plaintext) {
BitmessageAddress sendingAddress = plaintext.getFrom();
if (sendingAddress.getVersion() < 4) {
return new V4Broadcast(sendingAddress, plaintext);
} else {
return new V5Broadcast(sendingAddress, plaintext);
}
}
public static ObjectMessage createAck(Plaintext plaintext) {
if (plaintext == null || plaintext.getAckData() == null)
return null;
GenericPayload ack = new GenericPayload(3, plaintext.getFrom().getStream(), plaintext.getAckData());
return new ObjectMessage.Builder().objectType(MSG).payload(ack).expiresTime(UnixTime.now(plaintext.getTTL())).build();
}
}

View File

@ -24,7 +24,6 @@ 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.Security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,6 +32,7 @@ import java.io.IOException;
import java.io.InputStream;
import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* Creates protocol v3 network messages from {@link InputStream InputStreams}
@ -44,6 +44,9 @@ class V3MessageFactory {
findMagic(in);
String command = getCommand(in);
int length = (int) Decode.uint32(in);
if (length > 1600003) {
throw new NodeException("Payload of " + length + " bytes received, no more than 1600003 was expected.");
}
byte[] checksum = Decode.bytes(in, 4);
byte[] payloadBytes = Decode.bytes(in, length);
@ -59,7 +62,7 @@ class V3MessageFactory {
}
}
private static MessagePayload getPayload(String command, InputStream stream, int length) throws IOException {
static MessagePayload getPayload(String command, InputStream stream, int length) throws IOException {
switch (command) {
case "version":
return parseVersion(stream);
@ -73,12 +76,18 @@ class V3MessageFactory {
return parseGetData(stream);
case "object":
return readObject(stream, length);
case "custom":
return readCustom(stream, length);
default:
LOG.debug("Unknown command: " + command);
return null;
}
}
private static MessagePayload readCustom(InputStream in, int length) throws IOException {
return CustomMessage.read(in, length);
}
public static ObjectMessage readObject(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter();
byte nonce[] = Decode.bytes(in, 8, counter);
@ -92,18 +101,18 @@ class V3MessageFactory {
try {
ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length);
} catch (IOException e) {
} catch (Exception e) {
LOG.trace("Could not parse object payload - using generic payload instead", e);
payload = new GenericPayload(version, stream, data);
}
return new ObjectMessage.Builder()
.nonce(nonce)
.expiresTime(expiresTime)
.objectType(objectType)
.stream(stream)
.payload(payload)
.build();
.nonce(nonce)
.expiresTime(expiresTime)
.objectType(objectType)
.stream(stream)
.payload(payload)
.build();
}
private static GetData parseGetData(InputStream stream) throws IOException {
@ -144,13 +153,13 @@ class V3MessageFactory {
long[] streamNumbers = Decode.varIntList(stream);
return new Version.Builder()
.version(version)
.services(services)
.timestamp(timestamp)
.addrRecv(addrRecv).addrFrom(addrFrom)
.nonce(nonce)
.userAgent(userAgent)
.streams(streamNumbers).build();
.version(version)
.services(services)
.timestamp(timestamp)
.addrRecv(addrRecv).addrFrom(addrFrom)
.nonce(nonce)
.userAgent(userAgent)
.streams(streamNumbers).build();
}
private static InventoryVector parseInventoryVector(InputStream stream) throws IOException {
@ -170,11 +179,17 @@ class V3MessageFactory {
long services = Decode.int64(stream);
byte[] ipv6 = Decode.bytes(stream, 16);
int port = Decode.uint16(stream);
return new NetworkAddress.Builder().time(time).stream(streamNumber).services(services).ipv6(ipv6).port(port).build();
return new NetworkAddress.Builder()
.time(time)
.stream(streamNumber)
.services(services)
.ipv6(ipv6)
.port(port)
.build();
}
private static boolean testChecksum(byte[] checksum, byte[] payload) {
byte[] payloadChecksum = Security.sha512(payload);
byte[] payloadChecksum = cryptography().sha512(payload);
for (int i = 0; i < checksum.length; i++) {
if (checksum[i] != payloadChecksum[i]) {
return false;
@ -185,10 +200,10 @@ class V3MessageFactory {
private static String getCommand(InputStream stream) throws IOException {
byte[] bytes = new byte[12];
int end = -1;
int end = bytes.length;
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) stream.read();
if (end == -1) {
if (end == bytes.length) {
if (bytes[i] == 0) end = i;
} else {
if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command");

View File

@ -0,0 +1,189 @@
/*
* Copyright 2016 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.MessagePayload;
import ch.dissem.bitmessage.entity.NetworkMessage;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.exception.NodeException;
import ch.dissem.bitmessage.utils.Decode;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES;
import static ch.dissem.bitmessage.factory.BufferPool.bufferPool;
import static ch.dissem.bitmessage.ports.NetworkHandler.MAX_PAYLOAD_SIZE;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/**
* Similar to the {@link V3MessageFactory}, but used for NIO buffers which may or may not contain a whole message.
*/
public class V3MessageReader {
private ByteBuffer headerBuffer;
private ByteBuffer dataBuffer;
private ReaderState state = ReaderState.MAGIC;
private String command;
private int length;
private byte[] checksum;
private List<NetworkMessage> messages = new LinkedList<>();
public ByteBuffer getActiveBuffer() {
if (state != null && state != ReaderState.DATA) {
if (headerBuffer == null) {
headerBuffer = bufferPool.allocateHeaderBuffer();
}
}
return state == ReaderState.DATA ? dataBuffer : headerBuffer;
}
public void update() {
if (state != ReaderState.DATA) {
getActiveBuffer();
headerBuffer.flip();
}
switch (state) {
case MAGIC:
if (!findMagicBytes(headerBuffer)) {
headerBuffer.compact();
return;
}
state = ReaderState.HEADER;
case HEADER:
if (headerBuffer.remaining() < 20) {
headerBuffer.compact();
headerBuffer.limit(20);
return;
}
command = getCommand(headerBuffer);
length = (int) Decode.uint32(headerBuffer);
if (length > MAX_PAYLOAD_SIZE) {
throw new NodeException("Payload of " + length + " bytes received, no more than " +
MAX_PAYLOAD_SIZE + " was expected.");
}
checksum = new byte[4];
headerBuffer.get(checksum);
state = ReaderState.DATA;
bufferPool.deallocate(headerBuffer);
headerBuffer = null;
dataBuffer = bufferPool.allocate(length);
dataBuffer.clear();
dataBuffer.limit(length);
case DATA:
if (dataBuffer.position() < length) {
return;
} else {
dataBuffer.flip();
}
if (!testChecksum(dataBuffer)) {
state = ReaderState.MAGIC;
throw new NodeException("Checksum failed for message '" + command + "'");
}
try {
MessagePayload payload = V3MessageFactory.getPayload(
command,
new ByteArrayInputStream(dataBuffer.array(),
dataBuffer.arrayOffset() + dataBuffer.position(), length),
length);
if (payload != null) {
messages.add(new NetworkMessage(payload));
}
} catch (IOException e) {
throw new NodeException(e.getMessage());
} finally {
state = ReaderState.MAGIC;
bufferPool.deallocate(dataBuffer);
dataBuffer = null;
dataBuffer = null;
}
}
}
public List<NetworkMessage> getMessages() {
return messages;
}
private boolean findMagicBytes(ByteBuffer buffer) {
int i = 0;
while (buffer.hasRemaining()) {
if (i == 0) {
buffer.mark();
}
if (buffer.get() == MAGIC_BYTES[i]) {
i++;
if (i == MAGIC_BYTES.length) {
return true;
}
} else {
i = 0;
}
}
if (i > 0) {
buffer.reset();
}
return false;
}
private static String getCommand(ByteBuffer buffer) {
int start = buffer.position();
int l = 0;
while (l < 12 && buffer.get() != 0) l++;
int i = l + 1;
while (i < 12) {
if (buffer.get() != 0) throw new NodeException("'\\0' padding expected for command");
i++;
}
try {
return new String(buffer.array(), start, l, "ASCII");
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
}
private boolean testChecksum(ByteBuffer buffer) {
byte[] payloadChecksum = cryptography().sha512(buffer.array(),
buffer.arrayOffset() + buffer.position(), length);
for (int i = 0; i < checksum.length; i++) {
if (checksum[i] != payloadChecksum[i]) {
return false;
}
}
return true;
}
/**
* De-allocates all buffers. This method should be called iff the reader isn't used anymore, i.e. when its
* connection is severed.
*/
public void cleanup() {
state = null;
if (headerBuffer != null) {
bufferPool.deallocate(headerBuffer);
}
if (dataBuffer != null) {
bufferPool.deallocate(dataBuffer);
}
}
private enum ReaderState {MAGIC, HEADER, DATA}
}

View File

@ -0,0 +1,192 @@
/*
* 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.ports;
import ch.dissem.bitmessage.InternalContext;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.SecureRandom;
import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES;
import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE;
import static ch.dissem.bitmessage.utils.Numbers.max;
/**
* Implements everything that isn't directly dependent on either Spongy- or Bouncycastle.
*/
public abstract class AbstractCryptography implements Cryptography, InternalContext.ContextHolder {
protected static final Logger LOG = LoggerFactory.getLogger(Cryptography.class);
private static final SecureRandom RANDOM = new SecureRandom();
private static final BigInteger TWO = BigInteger.valueOf(2);
private static final BigInteger TWO_POW_64 = TWO.pow(64);
private static final BigInteger TWO_POW_16 = TWO.pow(16);
protected final Provider provider;
private InternalContext context;
protected AbstractCryptography(Provider provider) {
this.provider = provider;
}
@Override
public void setContext(InternalContext context) {
this.context = context;
}
public byte[] sha512(byte[] data, int offset, int length) {
MessageDigest mda = md("SHA-512");
mda.update(data, offset, length);
return mda.digest();
}
public byte[] sha512(byte[]... data) {
return hash("SHA-512", data);
}
public byte[] doubleSha512(byte[]... data) {
MessageDigest mda = md("SHA-512");
for (byte[] d : data) {
mda.update(d);
}
return mda.digest(mda.digest());
}
public byte[] doubleSha512(byte[] data, int length) {
MessageDigest mda = md("SHA-512");
mda.update(data, 0, length);
return mda.digest(mda.digest());
}
public byte[] ripemd160(byte[]... data) {
return hash("RIPEMD160", data);
}
public byte[] doubleSha256(byte[] data, int length) {
MessageDigest mda = md("SHA-256");
mda.update(data, 0, length);
return mda.digest(mda.digest());
}
public byte[] sha1(byte[]... data) {
return hash("SHA-1", data);
}
public byte[] randomBytes(int length) {
byte[] result = new byte[length];
RANDOM.nextBytes(result);
return result;
}
public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte,
long extraBytes, ProofOfWorkEngine.Callback callback) {
nonceTrialsPerByte = max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE);
extraBytes = max(extraBytes, NETWORK_EXTRA_BYTES);
byte[] initialHash = getInitialHash(object);
byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes);
context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback);
}
public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
throws IOException {
byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes);
byte[] value = doubleSha512(object.getNonce(), getInitialHash(object));
if (Bytes.lt(target, value, 8)) {
throw new InsufficientProofOfWorkException(target, value);
}
}
@Override
public byte[] getInitialHash(ObjectMessage object) {
return sha512(object.getPayloadBytesWithoutNonce());
}
@Override
public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
if (nonceTrialsPerByte == 0) nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE;
if (extraBytes == 0) extraBytes = NETWORK_EXTRA_BYTES;
BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now());
BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes);
BigInteger 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 byte[] hash(String algorithm, byte[]... data) {
MessageDigest mda = md(algorithm);
for (byte[] d : data) {
mda.update(d);
}
return mda.digest();
}
private MessageDigest md(String algorithm) {
try {
return MessageDigest.getInstance(algorithm, provider);
} catch (GeneralSecurityException e) {
throw new ApplicationException(e);
}
}
public byte[] mac(byte[] key_m, byte[] data) {
try {
Mac mac = Mac.getInstance("HmacSHA256", provider);
mac.init(new SecretKeySpec(key_m, "HmacSHA256"));
return mac.doFinal(data);
} catch (GeneralSecurityException e) {
throw new ApplicationException(e);
}
}
public Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey,
long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) {
return Factory.createPubkey(version, stream,
createPublicKey(privateSigningKey),
createPublicKey(privateEncryptionKey),
nonceTrialsPerByte, extraBytes, features);
}
public BigInteger keyToBigInt(byte[] privateKey) {
return new BigInteger(1, privateKey);
}
public long randomNonce() {
return RANDOM.nextLong();
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright 2016 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.Label;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Strings;
import ch.dissem.bitmessage.utils.UnixTime;
import java.util.Collection;
import java.util.List;
import static ch.dissem.bitmessage.utils.SqlStrings.join;
public abstract class AbstractMessageRepository implements MessageRepository, InternalContext.ContextHolder {
protected InternalContext ctx;
@Override
public void setContext(InternalContext context) {
this.ctx = context;
}
protected void safeSenderIfNecessary(Plaintext message) {
if (message.getId() == null) {
BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress());
if (savedAddress == null) {
ctx.getAddressRepository().save(message.getFrom());
} else if (savedAddress.getPubkey() == null && message.getFrom().getPubkey() != null) {
savedAddress.setPubkey(message.getFrom().getPubkey());
ctx.getAddressRepository().save(savedAddress);
}
}
}
@Override
public Plaintext getMessage(Object id) {
if (id instanceof Long) {
return single(find("id=" + id));
} else {
throw new IllegalArgumentException("Long expected for ID");
}
}
@Override
public Plaintext getMessage(byte[] initialHash) {
return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'"));
}
@Override
public Plaintext getMessageForAck(byte[] ackData) {
return single(find("ack_data=X'" + Strings.hex(ackData) + "' AND status='" + Plaintext.Status.SENT + "'"));
}
@Override
public List<Plaintext> findMessages(Label label) {
if (label == null) {
return find("id NOT IN (SELECT message_id FROM Message_Label)");
} else {
return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")");
}
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) {
return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'");
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status) {
return find("status='" + status.name() + "'");
}
@Override
public List<Plaintext> findMessages(BitmessageAddress sender) {
return find("sender='" + sender.getAddress() + "'");
}
@Override
public List<Plaintext> findMessagesToResend() {
return find("status='" + Plaintext.Status.SENT.name() + "'" +
" AND next_try < " + UnixTime.now());
}
@Override
public List<Label> getLabels() {
return findLabels("1=1");
}
@Override
public List<Label> getLabels(Label.Type... types) {
return findLabels("type IN (" + join(types) + ")");
}
protected abstract List<Label> findLabels(String where);
protected <T> T single(Collection<T> collection) {
switch (collection.size()) {
case 0:
return null;
case 1:
return collection.iterator().next();
default:
throw new ApplicationException("This shouldn't happen, found " + collection.size() +
" items, one or none was expected");
}
}
protected abstract List<Plaintext> find(String where);
}

View File

@ -30,12 +30,17 @@ public interface AddressRepository {
*/
List<BitmessageAddress> getIdentities();
/**
* @return all subscribed chans.
*/
List<BitmessageAddress> getChans();
List<BitmessageAddress> getSubscriptions();
List<BitmessageAddress> getSubscriptions(long broadcastVersion);
/**
* @return all Bitmessage addresses that have no private key.
* @return all Bitmessage addresses that have no private key or are chans.
*/
List<BitmessageAddress> getContacts();

View File

@ -0,0 +1,222 @@
/*
* 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.ports;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException;
import java.io.IOException;
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 {@link SecureRandom},
* which should be secure enough.
*/
public interface Cryptography {
/**
* A helper method to calculate SHA-512 hashes. Please note that a new {@link 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
*/
byte[] sha512(byte[] data, int offset, int length);
/**
* A helper method to calculate SHA-512 hashes. Please note that a new {@link 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
*/
byte[] sha512(byte[]... data);
/**
* A helper method to calculate doubleSHA-512 hashes. Please note that a new {@link 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
*/
byte[] doubleSha512(byte[]... data);
/**
* 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.
* <p>
* Please note that a new {@link 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.
* </p>
*
* @param data to get hashed
* @param length number of bytes to be taken into account
* @return SHA-512 hash of data
*/
byte[] doubleSha512(byte[] data, int length);
/**
* 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.
* <p>
* Please note that a new {@link 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.
* </p>
*
* @param data to get hashed
* @return RIPEMD-160 hash of data
*/
byte[] ripemd160(byte[]... data);
/**
* 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.
* <p>
* Please note that a new {@link 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.
* </p>
*
* @param data to get hashed
* @param length number of bytes to be taken into account
* @return SHA-256 hash of data
*/
byte[] doubleSha256(byte[] data, int length);
/**
* 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.
* <p>
* Please note that a new {@link 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.
* </p>
*
* @param data to get hashed
* @return SHA hash of data
*/
byte[] sha1(byte[]... data);
/**
* @param length number of bytes to return
* @return an array of the given size containing random bytes
*/
byte[] randomBytes(int length);
/**
* Calculates the proof of work. This might take a long time, depending on the hardware, message size and time to
* live.
*
* @param object 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
*/
void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte,
long extraBytes, ProofOfWorkEngine.Callback callback);
/**
* @param object 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)
*/
void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
throws IOException;
byte[] getInitialHash(ObjectMessage object);
byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
/**
* 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
*/
byte[] mac(byte[] key_m, byte[] data);
/**
* @param encrypt if true, encrypts data, otherwise tries to decrypt it.
* @param data
* @param key_e
* @return
*/
byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector);
/**
* 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
*/
Pubkey createPubkey(long version, long stream, byte[] privateSigningKey, byte[] privateEncryptionKey,
long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features);
/**
* @param privateKey private key as byte array
* @return a public key corresponding to the given private key
*/
byte[] createPublicKey(byte[] privateKey);
/**
* @param privateKey private key as byte array
* @return a big integer representation (unsigned) of the given bytes
*/
BigInteger keyToBigInt(byte[] privateKey);
/**
* @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
*/
boolean isSignatureValid(byte[] data, byte[] signature, Pubkey pubkey);
/**
* 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
*/
byte[] getSignature(byte[] data, ch.dissem.bitmessage.entity.valueobject.PrivateKey privateKey);
/**
* @return a random number of type long
*/
long randomNonce();
byte[] multiply(byte[] k, byte[] r);
byte[] createPoint(byte[] x, byte[] y);
}

View File

@ -16,25 +16,12 @@
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.utils.Property;
import java.io.IOException;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.MessagePayload;
/**
* Handles incoming messages
* @author Christian Basler
*/
public interface NetworkHandler {
void start(MessageListener listener);
void stop();
void offer(InventoryVector iv);
Property getNetworkStatus();
interface MessageListener {
void receive(ObjectMessage object) throws IOException;
}
public interface CustomCommandHandler {
MessagePayload handle(CustomMessage request);
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2016 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.valueobject.Label;
import static ch.dissem.bitmessage.entity.Plaintext.Status.*;
public class DefaultLabeler implements Labeler, InternalContext.ContextHolder {
private InternalContext ctx;
@Override
public void setLabels(Plaintext msg) {
msg.setStatus(RECEIVED);
if (msg.getType() == Plaintext.Type.BROADCAST) {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
} else {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD));
}
}
@Override
public void markAsDraft(Plaintext msg) {
msg.setStatus(DRAFT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.DRAFT));
}
@Override
public void markAsSending(Plaintext msg) {
if (msg.getTo() != null && msg.getTo().getPubkey() == null) {
msg.setStatus(PUBKEY_REQUESTED);
} else {
msg.setStatus(DOING_PROOF_OF_WORK);
}
msg.removeLabel(Label.Type.DRAFT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX));
}
@Override
public void markAsSent(Plaintext msg) {
msg.setStatus(SENT);
msg.removeLabel(Label.Type.OUTBOX);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT));
}
@Override
public void markAsAcknowledged(Plaintext msg) {
msg.setStatus(SENT_ACKNOWLEDGED);
}
@Override
public void markAsRead(Plaintext msg) {
msg.removeLabel(Label.Type.UNREAD);
}
@Override
public void markAsUnread(Plaintext msg) {
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.UNREAD));
}
@Override
public void delete(Plaintext msg) {
msg.getLabels().clear();
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.TRASH));
}
@Override
public void archive(Plaintext msg) {
msg.getLabels().clear();
}
@Override
public void setContext(InternalContext ctx) {
this.ctx = ctx;
}
}

View File

@ -26,15 +26,32 @@ import java.util.List;
* The Inventory stores and retrieves objects, cleans up outdated objects and can tell which objects are still missing.
*/
public interface Inventory {
/**
* Returns the IVs of all valid objects we have for the given streams
*/
List<InventoryVector> getInventory(long... streams);
/**
* 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.
*/
List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams);
ObjectMessage getObject(InventoryVector vector);
/**
* This method is mainly used to search for public keys to newly added addresses or broadcasts from new
* subscriptions.
*/
List<ObjectMessage> getObjects(long stream, long version, ObjectType... types);
void storeObject(ObjectMessage object);
boolean contains(ObjectMessage object);
/**
* Deletes all objects that expired 5 minutes ago or earlier
* (so we don't accidentally request objects we just deleted)
*/
void cleanup();
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 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 {@link DefaultLabeler} whenever possible,
* instead of directly implementing the interface.
* <p>
* As the labeler gets called whenever the state of a message changes, it can also be used
* as a listener.
* </p>
*/
public interface Labeler {
/**
* Sets the labels of a newly received message.
*
* @param msg an unlabeled message or broadcast
*/
void setLabels(Plaintext msg);
void markAsDraft(Plaintext msg);
/**
* It is paramount that this methods marks the {@link Plaintext} object with status
* {@link Plaintext.Status#PUBKEY_REQUESTED} (see {@link DefaultLabeler})
*/
void markAsSending(Plaintext msg);
void markAsSent(Plaintext msg);
void markAsAcknowledged(Plaintext msg);
void markAsRead(Plaintext msg);
void markAsUnread(Plaintext msg);
void delete(Plaintext msg);
void archive(Plaintext msg);
}

View File

@ -28,12 +28,24 @@ public interface MessageRepository {
List<Label> getLabels(Label.Type... types);
int countUnread(Label label);
Plaintext getMessage(Object id);
Plaintext getMessage(byte[] initialHash);
Plaintext getMessageForAck(byte[] ackData);
List<Plaintext> findMessages(Label label);
List<Plaintext> findMessages(Status status);
List<Plaintext> findMessages(Status status, BitmessageAddress recipient);
List<Plaintext> findMessages(BitmessageAddress sender);
List<Plaintext> findMessagesToResend();
void save(Plaintext message);
void remove(Plaintext message);

View File

@ -0,0 +1,128 @@
/*
* 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.ports;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import static ch.dissem.bitmessage.utils.Bytes.inc;
import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool;
/**
* A POW engine using all available CPU cores.
*/
public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
private static final Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class);
private final ExecutorService waiterPool = Executors.newSingleThreadExecutor(pool("POW-waiter").daemon().build());
private final ExecutorService 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
public void calculateNonce(final byte[] initialHash, final byte[] target, final Callback callback) {
waiterPool.execute(new Runnable() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
int cores = Runtime.getRuntime().availableProcessors();
if (cores > 255) cores = 255;
LOG.info("Doing POW using " + cores + " cores");
List<Worker> workers = new ArrayList<>(cores);
for (int i = 0; i < cores; i++) {
Worker w = new Worker((byte) cores, i, initialHash, target);
workers.add(w);
}
List<Future<byte[]>> futures = new ArrayList<>(cores);
for (Worker w : workers) {
// 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.
futures.add(workerPool.submit(w));
}
try {
while (!Thread.interrupted()) {
for (Future<byte[]> future : futures) {
if (future.isDone()) {
callback.onNonceCalculated(initialHash, future.get());
LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds");
for (Future<byte[]> f : futures) {
f.cancel(true);
}
return;
}
}
Thread.sleep(100);
}
LOG.error("POW waiter thread interrupted - this should not happen!");
} catch (ExecutionException e) {
LOG.error(e.getMessage(), e);
} catch (InterruptedException e) {
LOG.error("POW waiter thread interrupted - this should not happen!", e);
}
}
});
}
private class Worker implements Callable<byte[]> {
private final byte numberOfCores;
private final byte[] initialHash;
private final byte[] target;
private final MessageDigest mda;
private final byte[] nonce = new byte[8];
Worker(byte numberOfCores, int core, byte[] initialHash, byte[] target) {
this.numberOfCores = numberOfCores;
this.initialHash = initialHash;
this.target = target;
this.nonce[7] = (byte) core;
try {
mda = MessageDigest.getInstance("SHA-512");
} catch (NoSuchAlgorithmException e) {
LOG.error(e.getMessage(), e);
throw new ApplicationException(e);
}
}
@Override
public byte[] call() throws Exception {
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;
}
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.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.Collection;
import java.util.concurrent.Future;
/**
* Handles incoming messages
*/
public interface NetworkHandler {
int NETWORK_MAGIC_NUMBER = 8;
int HEADER_SIZE = 24;
int MAX_PAYLOAD_SIZE = 1600003;
int MAX_MESSAGE_SIZE = HEADER_SIZE + MAX_PAYLOAD_SIZE;
/**
* Connects to the trusted host, fetches and offers new messages and disconnects afterwards.
* <p>
* An implementation should disconnect if either the timeout is reached or the returned thread is interrupted.
* </p>
*/
Future<?> synchronize(InetAddress server, int port, long timeoutInSeconds);
/**
* Send a custom message to a specific node (that should implement handling for this message type) and returns
* the response, which in turn is expected to be a {@link CustomMessage}.
*
* @param server the node's address
* @param port the node's port
* @param request the request
* @return the response
*/
CustomMessage send(InetAddress server, int port, CustomMessage request);
/**
* Start a full network node, accepting incoming connections and relaying objects.
*/
void start();
/**
* Stop the full network node.
*/
void stop();
/**
* Offer new objects to up to 8 random nodes.
*/
void offer(InventoryVector iv);
/**
* Request each of those objects from a node that knows of the requested object.
*
* @param inventoryVectors of the objects to be requested
*/
void request(Collection<InventoryVector> inventoryVectors);
Property getNetworkStatus();
boolean isRunning();
interface MessageListener {
void receive(ObjectMessage object) throws IOException;
}
}

View File

@ -0,0 +1,54 @@
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
import ch.dissem.bitmessage.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.*;
/**
* Helper class to kick start node registries.
*/
public class NodeRegistryHelper {
private static final Logger LOG = LoggerFactory.getLogger(NodeRegistryHelper.class);
public static Map<Long, Set<NetworkAddress>> loadStableNodes() {
try (InputStream in = NodeRegistryHelper.class.getClassLoader().getResourceAsStream("nodes.txt")) {
Scanner scanner = new Scanner(in);
long stream = 0;
Map<Long, Set<NetworkAddress>> result = new HashMap<>();
Set<NetworkAddress> streamSet = null;
while (scanner.hasNext()) {
try {
String line = scanner.nextLine().trim();
if (line.startsWith("[stream")) {
stream = Long.parseLong(line.substring(8, line.lastIndexOf(']')));
streamSet = new HashSet<>();
result.put(stream, streamSet);
} else if (streamSet != null && !line.isEmpty() && !line.startsWith("#")) {
int portIndex = line.lastIndexOf(':');
InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex));
int port = Integer.valueOf(line.substring(portIndex + 1));
for (InetAddress inetAddress : inetAddresses) {
streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build());
}
}
} catch (IOException e) {
LOG.warn(e.getMessage(), e);
}
}
if (LOG.isDebugEnabled()) {
for (Map.Entry<Long, Set<NetworkAddress>> e : result.entrySet()) {
LOG.debug("Stream " + e.getKey() + ": loaded " + e.getValue().size() + " bootstrap nodes.");
}
}
return result;
} catch (IOException e) {
throw new ApplicationException(e);
}
}
}

View File

@ -24,9 +24,17 @@ public 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
* @return 8 bytes nonce
* @param initialHash the SHA-512 hash of the object to send, sans nonce
* @param target the target, representing an unsigned long
* @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make
* sure this is only called once.
*/
byte[] calculateNonce(byte[] initialHash, byte[] target);
void calculateNonce(byte[] initialHash, byte[] target, Callback callback);
interface Callback {
/**
* @param nonce 8 bytes nonce
*/
void onNonceCalculated(byte[] initialHash, byte[] nonce);
}
}

View File

@ -0,0 +1,45 @@
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import java.util.List;
/**
* Objects that proof of work is currently being done for.
*
* @author Christian Basler
*/
public interface ProofOfWorkRepository {
Item getItem(byte[] initialHash);
List<byte[]> getItems();
void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
void putObject(Item item);
void removeObject(byte[] initialHash);
class Item {
public final ObjectMessage object;
public final long nonceTrialsPerByte;
public final long extraBytes;
// Needed for ACK POW calculation
public final Long expirationTime;
public final Plaintext message;
public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
this(object, nonceTrialsPerByte, extraBytes, 0, null);
}
public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, long expirationTime, Plaintext message) {
this.object = object;
this.nonceTrialsPerByte = nonceTrialsPerByte;
this.extraBytes = extraBytes;
this.expirationTime = expirationTime;
this.message = message;
}
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.ports;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Bytes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static ch.dissem.bitmessage.utils.Bytes.inc;
/**
* You should really use the MultiThreadedPOWEngine, but this one might help you grok the other one.
* <p>
* <strong>Warning:</strong> implementations probably depend on POW being asynchronous, that's
* another reason not to use this one.
* </p>
*/
public class SimplePOWEngine implements ProofOfWorkEngine {
@Override
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
try {
MessageDigest mda = MessageDigest.getInstance("SHA-512");
byte[] nonce = new byte[8];
do {
inc(nonce);
mda.update(nonce);
mda.update(initialHash);
} while (Bytes.lt(target, mda.digest(mda.digest()), 8));
callback.onNonceCalculated(initialHash, nonce);
} catch (NoSuchAlgorithmException e) {
throw new ApplicationException(e);
}
}
}

View File

@ -18,6 +18,7 @@
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.exception.AddressFormatException;
import ch.dissem.bitmessage.exception.ApplicationException;
import java.io.UnsupportedEncodingException;
@ -30,7 +31,7 @@ import static java.util.Arrays.copyOfRange;
*/
public class Base58 {
private static final int[] INDEXES = new int[128];
private static char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
static {
for (int i = 0; i < INDEXES.length; i++) {
@ -44,27 +45,27 @@ public class Base58 {
/**
* Encodes the given bytes in base58. No checksum is appended.
*
* @param input to encode
* @param data to encode
* @return base58 encoded input
*/
public static String encode(byte[] input) {
if (input.length == 0) {
public static String encode(byte[] data) {
if (data.length == 0) {
return "";
}
input = copyOfRange(input, 0, input.length);
final byte[] bytes = copyOfRange(data, 0, data.length);
// Count leading zeroes.
int zeroCount = 0;
while (zeroCount < input.length && input[zeroCount] == 0) {
while (zeroCount < bytes.length && bytes[zeroCount] == 0) {
++zeroCount;
}
// The actual encoding.
byte[] temp = new byte[input.length * 2];
byte[] temp = new byte[bytes.length * 2];
int j = temp.length;
int startAt = zeroCount;
while (startAt < input.length) {
byte mod = divmod58(input, startAt);
if (input[startAt] == 0) {
while (startAt < bytes.length) {
byte mod = divmod58(bytes, startAt);
if (bytes[startAt] == 0) {
++startAt;
}
temp[--j] = (byte) ALPHABET[mod];
@ -83,7 +84,7 @@ public class Base58 {
try {
return new String(output, "US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Cannot happen.
throw new ApplicationException(e); // Cannot happen.
}
}
@ -97,7 +98,7 @@ public class Base58 {
char c = input.charAt(i);
int digit58 = -1;
if (c >= 0 && c < 128) {
if (c < 128) {
digit58 = INDEXES[c];
}
if (digit58 < 0) {

View File

@ -16,12 +16,6 @@
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.Streamable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* 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
@ -29,6 +23,8 @@ import java.util.Arrays;
* situations.
*/
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]++;
@ -88,9 +84,7 @@ public class Bytes {
}
private static boolean lt(byte a, byte b) {
if (a < 0) return b < 0 && a < b;
if (b < 0) return a >= 0 || a < b;
return a < b;
return (a ^ BYTE_0x80) < (b ^ BYTE_0x80);
}
/**

View File

@ -0,0 +1,48 @@
/*
* 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.utils;
/**
* Waits for a value within a callback method to be set.
*/
public class CallbackWaiter<T> {
private final long startTime = System.currentTimeMillis();
private volatile boolean isSet;
private T value;
private long time;
public void setValue(T value) {
synchronized (this) {
this.time = System.currentTimeMillis() - startTime;
this.value = value;
this.isSet = true;
}
}
public T waitForValue() throws InterruptedException {
while (!isSet) {
Thread.sleep(100);
}
synchronized (this) {
return value;
}
}
public long getTime() {
return time;
}
}

View File

@ -0,0 +1,152 @@
/*
* 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.utils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import static ch.dissem.bitmessage.utils.AccessCounter.inc;
/**
* This class handles decoding simple types from byte stream, according to
* https://bitmessage.org/wiki/Protocol_specification#Common_structures
*/
public class Decode {
public static byte[] shortVarBytes(InputStream in, AccessCounter counter) throws IOException {
int length = uint16(in, counter);
return bytes(in, length, counter);
}
public static byte[] varBytes(InputStream in) throws IOException {
return varBytes(in, null);
}
public static byte[] varBytes(InputStream in, AccessCounter counter) throws IOException {
int length = (int) varInt(in, counter);
return bytes(in, length, counter);
}
public static byte[] bytes(InputStream in, int count) throws IOException {
return bytes(in, count, null);
}
public static byte[] bytes(InputStream in, int count, AccessCounter counter) throws IOException {
byte[] result = new byte[count];
int off = 0;
while (off < count) {
int read = in.read(result, off, count - off);
if (read < 0) {
throw new IOException("Unexpected end of stream, wanted to read " + count + " bytes but only got " + off);
}
off += read;
}
inc(counter, count);
return result;
}
public static long[] varIntList(InputStream in) throws IOException {
int length = (int) varInt(in);
long[] result = new long[length];
for (int i = 0; i < length; i++) {
result[i] = varInt(in);
}
return result;
}
public static long varInt(InputStream in) throws IOException {
return varInt(in, null);
}
public static long varInt(InputStream in, AccessCounter counter) throws IOException {
int first = in.read();
inc(counter);
switch (first) {
case 0xfd:
return uint16(in, counter);
case 0xfe:
return uint32(in, counter);
case 0xff:
return int64(in, counter);
default:
return first;
}
}
public static int uint8(InputStream in) throws IOException {
return in.read();
}
public static int uint16(InputStream in) throws IOException {
return uint16(in, null);
}
public static int uint16(InputStream in, AccessCounter counter) throws IOException {
inc(counter, 2);
return in.read() << 8 | in.read();
}
public static long uint32(InputStream in) throws IOException {
return uint32(in, null);
}
public static long uint32(InputStream in, AccessCounter counter) throws IOException {
inc(counter, 4);
return in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
}
public static long uint32(ByteBuffer in) {
return u(in.get()) << 24 | u(in.get()) << 16 | u(in.get()) << 8 | u(in.get());
}
public static int int32(InputStream in) throws IOException {
return int32(in, null);
}
public static int int32(InputStream in, AccessCounter counter) throws IOException {
inc(counter, 4);
return ByteBuffer.wrap(bytes(in, 4)).getInt();
}
public static long int64(InputStream in) throws IOException {
return int64(in, null);
}
public static long int64(InputStream in, AccessCounter counter) throws IOException {
inc(counter, 8);
return ByteBuffer.wrap(bytes(in, 8)).getLong();
}
public static String varString(InputStream in) throws IOException {
return varString(in, null);
}
public static String varString(InputStream in, AccessCounter counter) throws IOException {
int length = (int) varInt(in, counter);
// FIXME: technically, it says the length in characters, but I think this one might be correct
// otherwise it will get complicated, as we'll need to read UTF-8 char by char...
return new String(bytes(in, length, counter), "utf-8");
}
/**
* Returns the given byte as if it were unsigned.
*/
private static int u(byte b) {
return b & 0xFF;
}
}

View File

@ -17,10 +17,13 @@
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.exception.ApplicationException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import static ch.dissem.bitmessage.utils.AccessCounter.inc;
@ -37,36 +40,54 @@ public class Encode {
}
}
public static void varIntList(long[] values, ByteBuffer buffer) {
varInt(values.length, buffer);
for (long value : values) {
varInt(value, buffer);
}
}
public static void varInt(long value, OutputStream stream) throws IOException {
varInt(value, stream, null);
}
public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException {
public static void varInt(long value, ByteBuffer buffer) {
if (value < 0) {
// This is due to the fact that Java doesn't really support unsigned values.
// Please be aware that this might be an error due to a smaller negative value being cast to long.
// Normally, negative values shouldn't occur within the protocol, and I large enough longs
// to being recognized as negatives aren't realistic.
stream.write(0xff);
inc(counter);
int64(value, stream, counter);
// Normally, negative values shouldn't occur within the protocol, and longs large enough for being
// recognized as negatives aren't realistic.
buffer.put((byte) 0xff);
buffer.putLong(value);
} else if (value < 0xfd) {
int8(value, stream, counter);
buffer.put((byte) value);
} else if (value <= 0xffffL) {
stream.write(0xfd);
inc(counter);
int16(value, stream, counter);
buffer.put((byte) 0xfd);
buffer.putShort((short) value);
} else if (value <= 0xffffffffL) {
stream.write(0xfe);
inc(counter);
int32(value, stream, counter);
buffer.put((byte) 0xfe);
buffer.putInt((int) value);
} else {
stream.write(0xff);
inc(counter);
int64(value, stream, counter);
buffer.put((byte) 0xff);
buffer.putLong(value);
}
}
public static byte[] varInt(long value) {
ByteBuffer buffer = ByteBuffer.allocate(9);
varInt(value, buffer);
buffer.flip();
return Bytes.truncate(buffer.array(), buffer.limit());
}
public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(9);
varInt(value, buffer);
buffer.flip();
stream.write(buffer.array(), 0, buffer.limit());
inc(counter, buffer.limit());
}
public static void int8(long value, OutputStream stream) throws IOException {
int8(value, stream, null);
}
@ -81,10 +102,14 @@ public class Encode {
}
public static void int16(long value, OutputStream stream, AccessCounter counter) throws IOException {
stream.write(ByteBuffer.allocate(4).putInt((int) value).array(), 2, 2);
stream.write(ByteBuffer.allocate(2).putShort((short) value).array());
inc(counter, 2);
}
public static void int16(long value, ByteBuffer buffer) {
buffer.putShort((short) value);
}
public static void int32(long value, OutputStream stream) throws IOException {
int32(value, stream, null);
}
@ -94,6 +119,10 @@ public class Encode {
inc(counter, 4);
}
public static void int32(long value, ByteBuffer buffer) {
buffer.putInt((int) value);
}
public static void int64(long value, OutputStream stream) throws IOException {
int64(value, stream, null);
}
@ -103,22 +132,57 @@ public class Encode {
inc(counter, 8);
}
public static void varString(String value, OutputStream stream) throws IOException {
public static void int64(long value, ByteBuffer buffer) {
buffer.putLong(value);
}
public static void varString(String value, OutputStream out) throws IOException {
byte[] bytes = value.getBytes("utf-8");
// FIXME: technically, it says the length in characters, but I think this one might be correct
// Technically, it says the length in characters, but I think this one might be correct.
// It doesn't really matter, as only ASCII characters are being used.
// see also Decode#varString()
varInt(bytes.length, stream);
stream.write(bytes);
varInt(bytes.length, out);
out.write(bytes);
}
public static void varString(String value, ByteBuffer buffer) {
try {
byte[] bytes = value.getBytes("utf-8");
// Technically, it says the length in characters, but I think this one might be correct.
// It doesn't really matter, as only ASCII characters are being used.
// see also Decode#varString()
buffer.put(varInt(bytes.length));
buffer.put(bytes);
} catch (UnsupportedEncodingException e) {
throw new ApplicationException(e);
}
}
public static void varBytes(byte[] data, OutputStream out) throws IOException {
varInt(data.length, out);
out.write(data);
}
public static void varBytes(byte[] data, ByteBuffer buffer) {
varInt(data.length, buffer);
buffer.put(data);
}
/**
* Serializes a {@link Streamable} object and returns the byte array.
*
* @param streamable the object to be serialized
* @return an array of bytes representing the given streamable object.
* @throws IOException if an I/O error occurs.
*/
public static byte[] bytes(Streamable streamable) throws IOException {
public static byte[] bytes(Streamable streamable) {
if (streamable == null) return null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
streamable.write(stream);
try {
streamable.write(stream);
} catch (IOException e) {
throw new ApplicationException(e);
}
return stream.toByteArray();
}
@ -126,11 +190,14 @@ public class Encode {
* @param streamable the object to be serialized
* @param padding the result will be padded such that its length is a multiple of <em>padding</em>
* @return the bytes of the given {@link Streamable} object, 0-padded such that the final length is x*padding.
* @throws IOException if an I/O error occurs.
*/
public static byte[] bytes(Streamable streamable, int padding) throws IOException {
public static byte[] bytes(Streamable streamable, int padding) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
streamable.write(stream);
try {
streamable.write(stream);
} catch (IOException e) {
throw new ApplicationException(e);
}
int offset = padding - stream.size() % padding;
int length = stream.size() + offset;
byte[] result = new byte[length];

View File

@ -0,0 +1,10 @@
package ch.dissem.bitmessage.utils;
/**
* @author Christian Basler
*/
public class Numbers {
public static long max(long a, long b) {
return a > b ? a : b;
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.utils;
import java.util.Arrays;
/**
* Created by chris on 20.07.15.
*/
public class Points {
public static byte[] getX(byte[] P) {
return Arrays.copyOfRange(P, 1, ((P.length - 1) / 2) + 1);
}
public static byte[] getY(byte[] P) {
return Arrays.copyOfRange(P, ((P.length - 1) / 2) + 1, P.length);
}
}

View File

@ -16,8 +16,16 @@
package ch.dissem.bitmessage.utils;
import java.util.Arrays;
import java.util.Objects;
/**
* Created by chris on 14.06.15.
* Some property that has a name, a value and/or other properties. This can be used for any purpose, but is for now
* used to contain different status information. It is by default displayed in some JSON inspired human readable
* notation, but you might only want to rely on the 'human readable' part.
* <p>
* If you need a real JSON representation, please add a method <code>toJson()</code>.
* </p>
*/
public class Property {
private String name;
@ -30,6 +38,36 @@ public class Property {
this.properties = properties;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
/**
* Returns the property if available or <code>null</code> otherwise.
* Subproperties can be requested by submitting the sequence of properties.
*/
public Property getProperty(String... name) {
if (name == null || name.length == 0) return null;
for (Property p : properties) {
if (Objects.equals(name[0], p.name)) {
if (name.length == 1)
return p;
else
return p.getProperty(Arrays.copyOfRange(name, 1, name.length));
}
}
return null;
}
public Property[] getProperties() {
return properties;
}
@Override
public String toString() {
return toString("");

View File

@ -0,0 +1,36 @@
/*
* 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.utils;
import ch.dissem.bitmessage.ports.Cryptography;
/**
* @author Christian Basler
*/
public class Singleton {
private static Cryptography cryptography;
public static void initialize(Cryptography cryptography) {
synchronized (Singleton.class) {
Singleton.cryptography = cryptography;
}
}
public static Cryptography cryptography() {
return cryptography;
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import static ch.dissem.bitmessage.utils.Strings.hex;
public class SqlStrings {
public static StringBuilder join(long... objects) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(objects[i]);
}
return streamList;
}
public static StringBuilder join(byte[]... objects) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(hex(objects[i]));
}
return streamList;
}
public static StringBuilder join(ObjectType... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(types[i].getNumber());
}
return streamList;
}
public static StringBuilder join(Enum... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append('\'').append(types[i].name()).append('\'');
}
return streamList;
}
}

View File

@ -16,8 +16,6 @@
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.payload.ObjectType;
/**
* Some utilities to handle strings.
* TODO: Probably this should be split in a GUI related and an SQL related utility class.

View File

@ -0,0 +1,40 @@
package ch.dissem.bitmessage.utils;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
/**
* Stores times to live for different object types. Usually this shouldn't be messed with,
* but for tests it might be a good idea to reduce it to a minimum, and on mobile clients
* you might want to optimize it as well.
*
* @author Christian Basler
*/
public class TTL {
private static long msg = 2 * DAY;
private static long getpubkey = 2 * DAY;
private static long pubkey = 28 * DAY;
public static long msg() {
return msg;
}
public static void msg(long msg) {
TTL.msg = msg;
}
public static long getpubkey() {
return getpubkey;
}
public static void getpubkey(long getpubkey) {
TTL.getpubkey = getpubkey;
}
public static long pubkey() {
return pubkey;
}
public static void pubkey(long pubkey) {
TTL.pubkey = pubkey;
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadFactoryBuilder {
private final String namePrefix;
private int prio = Thread.NORM_PRIORITY;
private boolean daemon = false;
private ThreadFactoryBuilder(String pool) {
this.namePrefix = pool + "-thread-";
}
public static ThreadFactoryBuilder pool(String name) {
return new ThreadFactoryBuilder(name);
}
public ThreadFactoryBuilder lowPrio() {
prio = Thread.MIN_PRIORITY;
return this;
}
public ThreadFactoryBuilder daemon() {
daemon = true;
return this;
}
public ThreadFactory build() {
SecurityManager s = System.getSecurityManager();
final ThreadGroup group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
return new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
t.setPriority(prio);
t.setDaemon(daemon);
return t;
}
};
}
}

View File

@ -0,0 +1,8 @@
[stream 1]
dissem.ch:8444
bootstrap8080.bitmessage.org:8080
bootstrap8444.bitmessage.org:8444
[stream 2]
# none yet

View File

@ -0,0 +1,315 @@
/*
* Copyright 2016 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.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.Plaintext.Type;
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.ports.*;
import ch.dissem.bitmessage.utils.MessageMatchers;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.TestUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Test;
import java.util.*;
import static ch.dissem.bitmessage.entity.payload.ObjectType.*;
import static ch.dissem.bitmessage.utils.MessageMatchers.object;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* @author Christian Basler
*/
public class BitmessageContextTest {
private BitmessageContext ctx;
private BitmessageContext.Listener listener;
@Before
public void setUp() throws Exception {
Singleton.initialize(null);
listener = mock(BitmessageContext.Listener.class);
ctx = new BitmessageContext.Builder()
.addressRepo(mock(AddressRepository.class))
.cryptography(new BouncyCryptography())
.inventory(mock(Inventory.class))
.listener(listener)
.messageRepo(mock(MessageRepository.class))
.networkHandler(mock(NetworkHandler.class))
.nodeRegistry(mock(NodeRegistry.class))
.labeler(spy(new DefaultLabeler()))
.powRepo(spy(new ProofOfWorkRepository() {
Map<InventoryVector, Item> items = new HashMap<>();
@Override
public Item getItem(byte[] initialHash) {
return items.get(new InventoryVector(initialHash));
}
@Override
public List<byte[]> getItems() {
List<byte[]> result = new LinkedList<>();
for (InventoryVector iv : items.keySet()) {
result.add(iv.getHash());
}
return result;
}
@Override
public void putObject(Item item) {
items.put(new InventoryVector(cryptography().getInitialHash(item.object)), item);
}
@Override
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
items.put(new InventoryVector(cryptography().getInitialHash(object)), new Item(object, nonceTrialsPerByte, extraBytes));
}
@Override
public void removeObject(byte[] initialHash) {
items.remove(initialHash);
}
}))
.proofOfWorkEngine(spy(new ProofOfWorkEngine() {
@Override
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
callback.onNonceCalculated(initialHash, new byte[8]);
}
}))
.build();
TTL.msg(2 * MINUTE);
}
@Test
public void ensureContactIsSavedAndPubkeyRequested() {
BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
ctx.addContact(contact);
verify(ctx.addresses(), times(2)).save(contact);
verify(ctx.internals().getProofOfWorkEngine())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensurePubkeyIsNotRequestedIfItExists() throws Exception {
ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload");
Pubkey pubkey = (Pubkey) object.getPayload();
BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
contact.setPubkey(pubkey);
ctx.addContact(contact);
verify(ctx.addresses(), times(1)).save(contact);
verify(ctx.internals().getProofOfWorkEngine(), never())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureV2PubkeyIsNotRequestedIfItExistsInInventory() throws Exception {
BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class)))
.thenReturn(Collections.singletonList(
TestUtils.loadObjectMessage(2, "V2Pubkey.payload")
));
ctx.addContact(contact);
verify(ctx.addresses(), atLeastOnce()).save(contact);
verify(ctx.internals().getProofOfWorkEngine(), never())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureV4PubkeyIsNotRequestedIfItExistsInInventory() throws Exception {
BitmessageAddress contact = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h");
when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class)))
.thenReturn(Collections.singletonList(
TestUtils.loadObjectMessage(2, "V4Pubkey.payload")
));
final BitmessageAddress stored = new BitmessageAddress(contact.getAddress());
stored.setAlias("Test");
when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(stored);
ctx.addContact(contact);
verify(ctx.addresses(), atLeastOnce()).save(argThat(new BaseMatcher<BitmessageAddress>() {
@Override
public boolean matches(Object item) {
return item instanceof BitmessageAddress
&& ((BitmessageAddress) item).getPubkey() != null
&& stored.getAlias().equals(((BitmessageAddress) item).getAlias());
}
@Override
public void describeTo(Description description) {
description.appendText("pubkey must not be null and alias must be ").appendValue(stored.getAlias());
}
}));
verify(ctx.internals().getProofOfWorkEngine(), never())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureSubscriptionIsAddedAndExistingBroadcastsRetrieved() throws Exception {
BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ");
List<ObjectMessage> objects = new LinkedList<>();
objects.add(TestUtils.loadObjectMessage(4, "V4Broadcast.payload"));
objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload"));
when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)))
.thenReturn(objects);
when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address));
ctx.addSubscribtion(address);
verify(ctx.addresses(), atLeastOnce()).save(address);
assertThat(address.isSubscribed(), is(true));
verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class));
verify(listener).receive(any(Plaintext.class));
}
@Test
public void ensureIdentityIsCreated() {
assertThat(ctx.createIdentity(false), notNullValue());
}
@Test
public void ensureMessageIsSent() throws Exception {
ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(),
"Subject", "Message");
assertEquals(2, ctx.internals().getProofOfWorkRepository().getItems().size());
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(MSG), eq(1000L), eq(1000L));
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG));
}
@Test
public void ensurePubkeyIsRequestedIfItIsMissing() throws Exception {
ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"),
new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"),
"Subject", "Message");
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(GET_PUBKEY), eq(1000L), eq(1000L));
verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG));
}
@Test(expected = IllegalArgumentException.class)
public void ensureSenderMustBeIdentity() {
ctx.send(new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"),
new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"),
"Subject", "Message");
}
@Test
public void ensureBroadcastIsSent() throws Exception {
ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"),
"Subject", "Message");
verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce())
.putObject(object(BROADCAST), eq(1000L), eq(1000L));
verify(ctx.internals().getProofOfWorkEngine())
.calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class));
verify(ctx.messages(), timeout(10000).atLeastOnce())
.save(MessageMatchers.plaintext(Type.BROADCAST));
}
@Test(expected = IllegalArgumentException.class)
public void ensureSenderWithoutPrivateKeyThrowsException() {
Plaintext msg = new Plaintext.Builder(Type.BROADCAST)
.from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.message("Subject", "Message")
.build();
ctx.send(msg);
}
@Test
public void ensureChanIsJoined() {
String chanAddress = "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r";
BitmessageAddress chan = ctx.joinChan("general", chanAddress);
assertNotNull(chan);
assertEquals(chan.getAddress(), chanAddress);
assertTrue(chan.isChan());
}
@Test
public void ensureDeterministicAddressesAreCreated() {
final int expected_size = 8;
List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, false);
assertEquals(expected_size, addresses.size());
Set<String> expected = new HashSet<>(expected_size);
expected.add("BM-2cWFkyuXXFw6d393RGnin2RpSXj8wxtt6F");
expected.add("BM-2cX8TF9vuQZEWvT7UrEeq1HN9dgiSUPLEN");
expected.add("BM-2cUzX8f9CKUU7L8NeB8GExZvf54PrcXq1S");
expected.add("BM-2cU7MAoQd7KE8SPF7AKFPpoEZKjk86KRqE");
expected.add("BM-2cVm8ByVBacc2DVhdTNs6rmy5ZQK6DUsrt");
expected.add("BM-2cW2af1vB6kWon2WkygDHqGwfcpfAFm2Jk");
expected.add("BM-2cWdWD7UtUN4gWChgNX9pvyvNPjUZvU8BT");
expected.add("BM-2cXkYgYcUrv4fGxSHzyEScW955Cc8sDteo");
for (BitmessageAddress a : addresses) {
assertTrue(expected.contains(a.getAddress()));
expected.remove(a.getAddress());
}
}
@Test
public void ensureShortDeterministicAddressesAreCreated() {
final int expected_size = 1;
List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, true);
assertEquals(expected_size, addresses.size());
Set<String> expected = new HashSet<>(expected_size);
expected.add("BM-NBGyBAEp6VnBkFWKpzUSgxuTqVdWPi78");
for (BitmessageAddress a : addresses) {
assertTrue(expected.contains(a.getAddress()));
expected.remove(a.getAddress());
}
}
@Test
public void ensureChanIsCreated() {
BitmessageAddress chan = ctx.createChan("test");
assertNotNull(chan);
assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION);
assertTrue(chan.isChan());
}
@Test
public void ensureUnacknowledgedMessageIsResent() throws Exception {
Plaintext plaintext = new Plaintext.Builder(Type.MSG)
.ttl(1)
.message("subject", "message")
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
.build();
assertTrue(plaintext.getTo().has(Pubkey.Feature.DOES_ACK));
when(ctx.messages().findMessagesToResend()).thenReturn(Collections.singletonList(plaintext));
when(ctx.messages().getMessage(any(byte[].class))).thenReturn(plaintext);
ctx.resendUnacknowledgedMessages();
verify(ctx.labeler(), timeout(1000).times(1)).markAsSent(eq(plaintext));
}
}

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.V4Broadcast;
import ch.dissem.bitmessage.entity.payload.V5Broadcast;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
@ -29,7 +30,7 @@ import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class DecryptionTest {
public class DecryptionTest extends TestBase {
@Test
public void ensureV4BroadcastIsDecryptedCorrectly() throws IOException, DecryptionFailedException {
BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ");

View File

@ -0,0 +1,157 @@
/*
* Copyright 2016 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.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.GetPubkey;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.MessageMatchers.plaintext;
import static org.mockito.Mockito.*;
/**
* @author Christian Basler
*/
public class DefaultMessageListenerTest extends TestBase {
@Mock
private AddressRepository addressRepo;
@Mock
private MessageRepository messageRepo;
@Mock
private Inventory inventory;
@Mock
private NetworkHandler networkHandler;
private InternalContext ctx;
private DefaultMessageListener listener;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
ctx = mock(InternalContext.class);
Singleton.initialize(new BouncyCryptography());
when(ctx.getAddressRepository()).thenReturn(addressRepo);
when(ctx.getMessageRepository()).thenReturn(messageRepo);
when(ctx.getInventory()).thenReturn(inventory);
when(ctx.getNetworkHandler()).thenReturn(networkHandler);
when(ctx.getLabeler()).thenReturn(mock(Labeler.class));
listener = new DefaultMessageListener(mock(Labeler.class), mock(BitmessageContext.Listener.class));
when(ctx.getNetworkListener()).thenReturn(listener);
listener.setContext(ctx);
}
@Test
public void ensurePubkeyIsSentOnRequest() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
when(addressRepo.findIdentity(any(byte[].class)))
.thenReturn(identity);
listener.receive(new ObjectMessage.Builder()
.stream(2)
.payload(new GetPubkey(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")))
.build());
verify(ctx).sendPubkey(eq(identity), eq(2L));
}
@Test
public void ensureIncomingPubkeyIsAddedToContact() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress contact = new BitmessageAddress(identity.getAddress());
when(addressRepo.findContact(any(byte[].class)))
.thenReturn(contact);
when(messageRepo.findMessages(eq(PUBKEY_REQUESTED), eq(contact)))
.thenReturn(Collections.singletonList(
new Plaintext.Builder(MSG).from(identity).to(contact).message("S", "T").build()
));
ObjectMessage objectMessage = new ObjectMessage.Builder()
.stream(2)
.payload(identity.getPubkey())
.build();
objectMessage.sign(identity.getPrivateKey());
objectMessage.encrypt(Singleton.cryptography().createPublicKey(identity.getPublicDecryptionKey()));
listener.receive(objectMessage);
verify(addressRepo).save(any(BitmessageAddress.class));
}
@Test
public void ensureIncomingMessageIsSaved() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress contact = new BitmessageAddress(identity.getAddress());
contact.setPubkey(identity.getPubkey());
when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity));
ObjectMessage objectMessage = new ObjectMessage.Builder()
.stream(2)
.payload(new Msg(new Plaintext.Builder(MSG)
.from(identity)
.to(contact)
.message("S", "T")
.build()))
.nonce(new byte[8])
.build();
objectMessage.sign(identity.getPrivateKey());
objectMessage.encrypt(identity.getPubkey());
listener.receive(objectMessage);
verify(messageRepo, atLeastOnce()).save(plaintext(MSG));
}
@Test
public void ensureIncomingBroadcastIsSaved() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
when(addressRepo.getSubscriptions(anyLong())).thenReturn(Collections.singletonList(identity));
Broadcast broadcast = Factory.getBroadcast(new Plaintext.Builder(BROADCAST)
.from(identity)
.message("S", "T")
.build());
ObjectMessage objectMessage = new ObjectMessage.Builder()
.stream(2)
.payload(broadcast)
.nonce(new byte[8])
.build();
objectMessage.sign(identity.getPrivateKey());
broadcast.encrypt();
listener.receive(objectMessage);
verify(messageRepo, atLeastOnce()).save(plaintext(BROADCAST));
}
}

View File

@ -24,25 +24,25 @@ import ch.dissem.bitmessage.entity.payload.GenericPayload;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.Security;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
import java.io.IOException;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class EncryptionTest {
public class EncryptionTest extends TestBase {
@Test
public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException {
GenericPayload before = new GenericPayload(0, 1, Security.randomBytes(100));
GenericPayload before = new GenericPayload(0, 1, cryptography().randomBytes(100));
PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000);
CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey());
GenericPayload after = GenericPayload.read(0, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 1, 100);
GenericPayload after = GenericPayload.read(0, 1, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 100);
assertEquals(before, after);
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2016 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.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.ports.*;
import ch.dissem.bitmessage.utils.Singleton;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
/**
* @author Christian Basler
*/
public class ProofOfWorkServiceTest {
private ProofOfWorkService proofOfWorkService;
private Cryptography cryptography;
@Mock
private InternalContext ctx;
@Mock
private ProofOfWorkRepository proofOfWorkRepo;
@Mock
private Inventory inventory;
@Mock
private NetworkHandler networkHandler;
@Mock
private MessageRepository messageRepo;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
cryptography = spy(new BouncyCryptography());
Singleton.initialize(cryptography);
ctx = mock(InternalContext.class);
when(ctx.getProofOfWorkRepository()).thenReturn(proofOfWorkRepo);
when(ctx.getInventory()).thenReturn(inventory);
when(ctx.getNetworkHandler()).thenReturn(networkHandler);
when(ctx.getMessageRepository()).thenReturn(messageRepo);
when(ctx.getLabeler()).thenReturn(mock(Labeler.class));
when(ctx.getNetworkListener()).thenReturn(mock(NetworkHandler.MessageListener.class));
proofOfWorkService = new ProofOfWorkService();
proofOfWorkService.setContext(ctx);
}
@Test
public void ensureMissingProofOfWorkIsDone() {
when(proofOfWorkRepo.getItems()).thenReturn(Arrays.asList(new byte[64]));
when(proofOfWorkRepo.getItem(any(byte[].class))).thenReturn(new ProofOfWorkRepository.Item(null, 1001, 1002));
doNothing().when(cryptography).doProofOfWork(any(ObjectMessage.class), anyLong(), anyLong(), any(ProofOfWorkEngine.Callback.class));
proofOfWorkService.doMissingProofOfWork(10);
verify(cryptography, timeout(1000)).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L),
any(ProofOfWorkEngine.Callback.class));
}
@Test
public void ensureCalculatedNonceIsStored() throws Exception {
BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
BitmessageAddress address = TestUtils.loadContact();
Plaintext plaintext = new Plaintext.Builder(MSG).from(identity).to(address).message("", "").build();
ObjectMessage object = new ObjectMessage.Builder()
.payload(new Msg(plaintext))
.build();
object.sign(identity.getPrivateKey());
object.encrypt(address.getPubkey());
byte[] initialHash = new byte[64];
byte[] nonce = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
when(proofOfWorkRepo.getItem(initialHash)).thenReturn(new ProofOfWorkRepository.Item(object, 1001, 1002));
when(messageRepo.getMessage(initialHash)).thenReturn(plaintext);
proofOfWorkService.onNonceCalculated(initialHash, nonce);
verify(proofOfWorkRepo).removeObject(eq(initialHash));
verify(inventory).storeObject(eq(object));
verify(networkHandler).offer(eq(object.getInventoryVector()));
assertThat(plaintext.getInventoryVector(), equalTo(object.getInventoryVector()));
}
}

View File

@ -22,18 +22,17 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
import java.io.IOException;
import java.util.Date;
import static org.junit.Assert.*;
public class SignatureTest {
public class SignatureTest extends TestBase {
@Test
public void ensureValidationWorks() throws IOException {
ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload");

View File

@ -27,9 +27,18 @@ import java.io.IOException;
import java.util.Arrays;
import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK;
import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.INCLUDE_DESTINATION;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.*;
public class BitmessageAddressTest {
public class BitmessageAddressTest extends TestBase {
@Test
public void ensureFeatureFlagIsCalculatedCorrectly() {
assertEquals(1, Pubkey.Feature.bitfield(DOES_ACK));
assertEquals(2, Pubkey.Feature.bitfield(INCLUDE_DESTINATION));
assertEquals(3, Pubkey.Feature.bitfield(DOES_ACK, INCLUDE_DESTINATION));
}
@Test
public void ensureBase58DecodesCorrectly() {
assertHexEquals("800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D",
@ -54,44 +63,59 @@ public class BitmessageAddressTest {
}
@Test
public void testCreateAddress() {
public void ensureIdentityCanBeCreated() {
BitmessageAddress address = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK));
assertNotNull(address.getPubkey());
assertTrue(address.has(DOES_ACK));
}
@Test
public void testV2PubkeyImport() throws IOException {
public void ensureV2PubkeyCanBeImported() throws IOException {
ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload");
Pubkey pubkey = (Pubkey) object.getPayload();
BitmessageAddress address = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT");
address.setPubkey(pubkey);
try {
address.setPubkey(pubkey);
} catch (Exception e) {
fail(e.getMessage());
}
}
@Test
public void testV3PubkeyImport() throws IOException {
public void ensureV3PubkeyCanBeImported() throws IOException {
BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ");
assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe());
ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload");
Pubkey pubkey = (Pubkey) object.getPayload();
assertTrue(object.isSignatureValid(pubkey));
address.setPubkey(pubkey);
try {
address.setPubkey(pubkey);
} catch (Exception e) {
fail(e.getMessage());
}
assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe());
assertTrue(address.has(DOES_ACK));
}
@Test
public void testV4PubkeyImport() throws IOException, DecryptionFailedException {
public void ensureV4PubkeyCanBeImported() throws IOException, DecryptionFailedException {
BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h");
ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload");
object.decrypt(address.getPublicDecryptionKey());
V4Pubkey pubkey = (V4Pubkey) object.getPayload();
assertTrue(object.isSignatureValid(pubkey));
address.setPubkey(pubkey);
try {
address.setPubkey(pubkey);
} catch (Exception e) {
fail(e.getMessage());
}
assertTrue(address.has(DOES_ACK));
}
@Test
public void testV3AddressImport() throws IOException {
public void ensureV3IdentityCanBeImported() throws IOException {
String address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn";
assertEquals(3, new BitmessageAddress(address_string).getVersion());
assertEquals(1, new BitmessageAddress(address_string).getStream());
@ -102,14 +126,22 @@ public class BitmessageAddressTest {
System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
Security.createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000)));
cryptography().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals(address_string, address.getAddress());
}
@Test
public void testGetSecret() throws IOException {
assertHexEquals("0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D",
getSecret("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ"));
public void ensureV4IdentityCanBeImported() throws IOException {
assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion());
byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU");
byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
cryptography().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress());
}
private void assertHexEquals(String hex, byte[] bytes) {
assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase());
}
private byte[] getSecret(String walletImportFormat) throws IOException {
@ -119,24 +151,10 @@ public class BitmessageAddressTest {
if (bytes.length != 37)
throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long");
byte[] hash = Security.doubleSha256(bytes, 33);
byte[] hash = cryptography().doubleSha256(bytes, 33);
for (int i = 0; i < 4; i++) {
if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat);
}
return Arrays.copyOfRange(bytes, 1, 33);
}
@Test
public void testV4AddressImport() throws IOException {
assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion());
byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU");
byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz");
BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey,
Security.createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000)));
assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress());
}
private void assertHexEquals(String hex, byte[] bytes) {
assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase());
}
}

View File

@ -17,21 +17,23 @@
package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.*;
public class SerializationTest {
public class SerializationTest extends TestBase {
@Test
public void ensureGetPubkeyIsDeserializedAndSerializedCorrectly() throws IOException {
doTest("V2GetPubkey.payload", 2, GetPubkey.class);
@ -75,28 +77,96 @@ public class SerializationTest {
}
@Test
public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws IOException, DecryptionFailedException {
public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws Exception {
Plaintext p1 = new Plaintext.Builder(MSG)
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
.message("Subject", "Message")
.ack("ack".getBytes())
.ackData("ackMessage".getBytes())
.signature(new byte[0])
.build();
ByteArrayOutputStream out = new ByteArrayOutputStream();
p1.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
Plaintext p2 = Plaintext.read(MSG, in);
// Received is automatically set on deserialization, so we'll need to set it to 0
Field received = Plaintext.class.getDeclaredField("received");
received.setAccessible(true);
received.set(p2, 0L);
assertEquals(p1, p2);
}
@Test
public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception {
Plaintext p1 = new Plaintext.Builder(MSG)
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
.message("Subject", "Message")
.ackData("ackMessage".getBytes())
.signature(new byte[0])
.build();
ObjectMessage ackMessage1 = p1.getAckMessage();
assertNotNull(ackMessage1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
p1.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
Plaintext p2 = Plaintext.read(MSG, in);
// Received is automatically set on deserialization, so we'll need to set it to 0
Field received = Plaintext.class.getDeclaredField("received");
received.setAccessible(true);
received.set(p2, 0L);
assertEquals(p1, p2);
assertEquals(ackMessage1, p2.getAckMessage());
}
@Test
public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception {
ArrayList<InventoryVector> ivs = new ArrayList<>(50000);
for (int i = 0; i < 50000; i++) {
ivs.add(new InventoryVector(cryptography().randomBytes(32)));
}
Inv inv = new Inv.Builder().inventory(ivs).build();
NetworkMessage before = new NetworkMessage(inv);
ByteArrayOutputStream out = new ByteArrayOutputStream();
before.write(out);
NetworkMessage after = Factory.getNetworkMessage(3, new ByteArrayInputStream(out.toByteArray()));
assertNotNull(after);
Inv invAfter = (Inv) after.getPayload();
assertEquals(ivs, invAfter.getInventory());
}
private void doTest(String resourceName, int version, Class<?> expectedPayloadType) throws IOException {
byte[] data = TestUtils.getBytes(resourceName);
InputStream in = new ByteArrayInputStream(data);
ObjectMessage object = Factory.getObjectMessage(version, in, data.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertNotNull(object);
object.write(out);
assertArrayEquals(data, out.toByteArray());
assertEquals(expectedPayloadType.getCanonicalName(), object.getPayload().getClass().getCanonicalName());
}
@Test
public void ensureSystemSerializationWorks() throws Exception {
Plaintext plaintext = new Plaintext.Builder(MSG)
.from(TestUtils.loadContact())
.to(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.labels(Collections.singletonList(new Label("Test", Label.Type.INBOX, 0)))
.message("Test", "Test Test.\nTest")
.build();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(plaintext);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
assertEquals(plaintext, ois.readObject());
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.ports;
import ch.dissem.bitmessage.utils.Bytes;
import ch.dissem.bitmessage.utils.CallbackWaiter;
import ch.dissem.bitmessage.utils.TestBase;
import org.junit.Test;
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
import static org.junit.Assert.assertTrue;
public class ProofOfWorkEngineTest extends TestBase {
@Test(timeout = 90_000)
public void testSimplePOWEngine() throws InterruptedException {
testPOW(new SimplePOWEngine());
}
@Test(timeout = 90_000)
public void testThreadedPOWEngine() throws InterruptedException {
testPOW(new MultiThreadedPOWEngine());
}
private void testPOW(ProofOfWorkEngine engine) throws InterruptedException {
byte[] initialHash = cryptography().sha512(new byte[]{1, 3, 6, 4});
byte[] target = {0, 0, 0, -1, -1, -1, -1, -1};
final CallbackWaiter<byte[]> waiter1 = new CallbackWaiter<>();
engine.calculateNonce(initialHash, target,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter1.setValue(nonce);
}
});
byte[] nonce = waiter1.waitForValue();
System.out.println("Calculating nonce took " + waiter1.getTime() + "ms");
assertTrue(Bytes.lt(cryptography().doubleSha512(nonce, initialHash), target, 8));
// Let's add a second (shorter) run to find possible multi threading issues
byte[] initialHash2 = cryptography().sha512(new byte[]{1, 3, 6, 5});
byte[] target2 = {0, 0, -1, -1, -1, -1, -1, -1};
final CallbackWaiter<byte[]> waiter2 = new CallbackWaiter<>();
engine.calculateNonce(initialHash2, target2,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter2.setValue(nonce);
}
});
byte[] nonce2 = waiter2.waitForValue();
System.out.println("Calculating nonce took " + waiter2.getTime() + "ms");
assertTrue(Bytes.lt(cryptography().doubleSha512(nonce2, initialHash2), target2, 8));
assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime());
}
}

View File

@ -16,6 +16,7 @@
package ch.dissem.bitmessage.utils;
import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
@ -25,16 +26,13 @@ import java.util.Random;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/**
* Created by chris on 10.04.15.
*/
public class BytesTest {
public static final Random rnd = new Random();
@Test
public void ensureExpandsCorrectly() {
byte[] source = {1};
byte[] expected = {0,1};
byte[] expected = {0, 1};
assertArrayEquals(expected, Bytes.expand(source, 2));
}
@ -56,6 +54,24 @@ public class BytesTest {
}
}
/**
* This test is used to compare different implementations of the single byte lt comparison. It an safely be ignored.
*/
@Test
@Ignore
public void testLowerThanSingleByte() {
byte[] a = new byte[1];
byte[] b = new byte[1];
for (int i = 0; i < 255; i++) {
for (int j = 0; j < 255; j++) {
System.out.println("a = " + i + "\tb = " + j);
a[0] = (byte) i;
b[0] = (byte) j;
assertEquals(i < j, Bytes.lt(a, b));
}
}
}
@Test
public void testLowerThan() {
for (int i = 0; i < 1000; i++) {

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