Compare commits

...

186 Commits
0.1 ... master

Author SHA1 Message Date
Sarah Jamie Lewis f82c2f9da4 Merge branch 'windows' of openprivacy/libricochet-go into master 2020-01-27 11:24:29 -08:00
Dan Ballard 7c828d3916 Use new Bine CmdCreatorFunc to make a ProcessCreator to hide Tor dos window.
Also hide it in checkTorVersion
2020-01-24 18:19:36 -05:00
Dan Ballard 79a1ff9161 Merge branch 'race' of openprivacy/libricochet-go into master 2019-11-07 16:26:35 -08:00
Sarah Jamie Lewis 5a1fc1b94d Fixing Race Conditions 2019-11-07 16:21:15 -08:00
Dan Ballard 9ba39b93b7 Merge branch 'restart-new' of openprivacy/libricochet-go into master 2019-11-06 12:07:47 -08:00
Sarah Jamie Lewis 29540dcf71 Expose Reboot 2019-11-06 11:59:29 -08:00
Dan Ballard 0fdcfd1553 drone liny 2019-11-06 11:57:14 -08:00
Dan Ballard ac4993adb7 drone get/download 2019-11-06 11:52:22 -08:00
Dan Ballard 881ca5c6c2 drone use go mod instead of list xargs for fetch 2019-11-06 11:50:25 -08:00
Dan Ballard 61f89d7b2c drone force modules 2019-11-06 11:40:58 -08:00
Sarah Jamie Lewis 725f64020a Merge branch 'logfile' of dan/libricochet-go into master 2019-08-12 14:47:10 -07:00
Dan Ballard b7cca3fa83 log now suports creating a logger around a file 2019-08-12 14:17:17 -07:00
Sarah Jamie Lewis 07747c4dd2 Merge branch 'torFix' of dan/libricochet-go into master 2019-07-31 15:09:46 -07:00
Dan Ballard 59ea2902e8 move .Dial outside of lock to stop throtteling one conn per time; get Dialer once only 2019-07-31 14:41:07 -07:00
Sarah Jamie Lewis 4ccdc79526 Merge branch 'torStatus' of dan/libricochet-go into master 2019-07-10 13:16:21 -07:00
Dan Ballard 6517665498 ACN and tor provider: take callback for status change 2019-07-10 13:08:49 -07:00
Sarah Jamie Lewis cd872e9e0a Merge branch 'logExclude' of dan/libricochet-go into master 2019-06-21 15:41:32 -07:00
Dan Ballard b534ecd04e add log exclude 2019-06-21 15:34:46 -07:00
Sarah Jamie Lewis aca0f63dd2 Merge branch 'nonewnym' of dan/libricochet-go into master 2019-02-20 19:45:31 +00:00
Dan Ballard bf57db657a stop issueing tor NEWNYM commands on errrors on open() 2019-02-20 11:36:42 -08:00
Sarah Jamie Lewis 8db3c09fce Merge branch 'tornetwork' of dan/libricochet-go into master 2019-02-14 19:50:29 +00:00
Dan Ballard fa12b3dcb9 when strting tor, dont let the network be disabled 2019-02-14 11:40:57 -08:00
erinn 9e4e042ffb Merge branch 'master' of dan/libricochet-go into master 2019-02-05 19:57:33 +00:00
Dan Ballard d78488a200 torprovide more nil checks 2019-02-05 11:54:52 -08:00
Sarah Jamie Lewis b3d6e0e019 Merge branch 'master' of dan/libricochet-go into master 2019-02-04 22:19:53 +00:00
Dan Ballard bf28d6574f prevent sigfault when tor is closed 2019-02-04 14:12:16 -08:00
Sarah Jamie Lewis 0739119a4d Merge branch 'v1-define1' of openprivacy/libricochet-go into master 2019-01-28 22:22:05 +00:00
Sarah Jamie Lewis 527ba61de0 Fixing V1 Error Issue with go mod 2019-01-28 14:20:14 -08:00
erinn af195d186c Merge branch 'v1-define' of openprivacy/libricochet-go into master 2019-01-28 22:07:20 +00:00
Sarah Jamie Lewis 5bfb17588c Defining Version 1.0 2019-01-28 14:01:57 -08:00
Dan Ballard 1131bc930e Merge branch 'v1-cleanup' of openprivacy/libricochet-go into master 2019-01-28 19:56:29 +00:00
Sarah Jamie Lewis 877f01a358 Cleaning up ineffectual error checking and misspellings 2019-01-26 14:05:09 -08:00
Dan Ballard 46e3eeeb7d Merge branch 'v1-cleanup' of openprivacy/libricochet-go into master 2019-01-23 21:10:36 +00:00
Sarah Jamie Lewis 96fc03580b Officially Break backwards-compatitbility with Ricochet-IM 2019-01-23 12:26:32 -08:00
Sarah Jamie Lewis 7a4350f0c1 Delete last reminants of V2 Onion Handling 2019-01-23 11:55:42 -08:00
Sarah Jamie Lewis b2f6b314fc Merge branch 'v1-cleanup' of openprivacy/libricochet-go into master 2019-01-23 19:47:30 +00:00
Sarah Jamie Lewis 64ce11d436 Cleaning up and documenting examples 2019-01-23 11:38:54 -08:00
erinn 8f00e26b81 Merge branch 'v1-encrypt' of openprivacy/libricochet-go into master 2019-01-23 19:38:13 +00:00
Sarah Jamie Lewis a96f682e77 Start using the derived ephemeral session key for encrypting 2019-01-23 11:31:44 -08:00
Dan Ballard 487b1e9ae0 Merge branch 'v1' of openprivacy/libricochet-go into master
<3
2019-01-23 01:19:46 +00:00
Sarah Jamie Lewis 9a680cd257 Renaming Instance and InstanceFactory
Now up to standard with go lint
2019-01-21 14:52:26 -08:00
Dan Ballard e068de0ef8 Merge branch 'close' of openprivacy/libricochet-go into master 2019-01-21 20:30:21 +00:00
Dan Ballard 1f0e87b3c4 Merge branch 'module_update' of openprivacy/libricochet-go into master 2019-01-21 20:28:52 +00:00
Sarah Jamie Lewis f4fbd52d4b go mod tidy 2019-01-21 11:34:27 -08:00
Sarah Jamie Lewis d87a0fcb52 Add Close() Method to Connection.
Explicitly Close Connection
2019-01-21 11:19:49 -08:00
Dan Ballard 859cdf95e1 Merge branch 'module' of openprivacy/libricochet-go into master 2019-01-14 20:03:12 +00:00
Dan Ballard 3137689de8 Merge branch 'update_readme' of openprivacy/libricochet-go into master 2019-01-14 19:51:42 +00:00
Sarah Jamie Lewis 2cf7113f80 Defining a Go Module 2019-01-13 14:20:42 -08:00
Sarah Jamie Lewis 4c23e439f0 First cut at 1.0 README 2019-01-11 20:46:31 +00:00
Dan Ballard 540e68b8ef Merge branch 'cleanup' of openprivacy/libricochet-go into master 2019-01-11 20:01:07 +00:00
Sarah Jamie Lewis b05567fd81 Fixing Linting, Vetting & Formatting Issues 2019-01-09 15:02:09 -08:00
Dan Ballard 8d1cba2b0b Merge branch 'remove-v3-contact-request' of openprivacy/libricochet-go into master 2019-01-09 22:08:40 +00:00
Sarah Jamie Lewis 90231b0be9 Remove V3 Contact Request 2019-01-08 11:09:23 -08:00
erinn 328ffc9d76 Merge branch 'tor-attach' of dan/libricochet-go into master 2018-12-18 19:49:29 +00:00
Dan Ballard 489bf62ff7 torProvider / listen now generates determinitic local ports, and can handle tor 550 detached onion exists, and connect to it 2018-12-07 10:41:38 -08:00
Dan Ballard b34fe84917 log api typo 2018-12-03 13:27:21 -08:00
Sarah Jamie Lewis b6caf3fbc4 Merge branch 'logging' of dan/libricochet-go into master 2018-12-03 20:19:59 +00:00
Dan Ballard 2815e29704 adding new filterable logging system 2018-12-03 11:59:21 -08:00
Sarah Jamie Lewis fa720940d8 Merge branch 'tor-retry' of dan/libricochet-go into master 2018-11-26 21:43:16 +00:00
Dan Ballard 5697a1c03d torProvider now gracefuly handles tor process fails 2018-11-26 12:51:08 -08:00
Sarah Jamie Lewis 232849e304 Merge branch 'acn2' of dan/libricochet-go into master 2018-11-23 03:18:24 +00:00
Dan Ballard 38cff4212d rename local variables acn; Add bootstrap status support to ACN/torprovider 2018-11-21 22:15:35 -08:00
Sarah Jamie Lewis b8a7cd702a Merge branch 'errmsg' of openprivacy/libricochet-go into master 2018-11-22 00:21:42 +00:00
erinn 65fb3992a3 Merge branch 'acn' of dan/libricochet-go into master 2018-11-22 00:20:19 +00:00
erinn 52bbc23251 include error message on rejected channel 2018-11-21 16:18:07 -08:00
Dan Ballard 53e6f90925 Merge branch 'master' of https://git.openprivacy.ca/openprivacy/libricochet-go 2018-11-21 15:55:21 -08:00
Dan Ballard b1fb04a880 drone only email on fail 2018-11-21 15:55:11 -08:00
Dan Ballard 0d080e4332 name update 2018-11-21 15:07:57 -08:00
Dan Ballard 4da220b223 Merge branch 'validhostname' of openprivacy/libricochet-go into master 2018-11-21 22:10:33 +00:00
Sarah Jamie Lewis d54ed0b106 Adding IsValidHostname function 2018-11-21 14:07:13 -08:00
Dan Ballard 52ce7615c3 Merge branch 'test_fix' of openprivacy/libricochet-go into master 2018-11-21 04:43:14 +00:00
Sarah Jamie Lewis aba5fa4609 Fixing Regex Bug 2018-11-20 20:33:11 -08:00
Dan Ballard 6c0d17667b Merge branch 'test_fix' of openprivacy/libricochet-go into master 2018-11-20 19:49:10 +00:00
Sarah Jamie Lewis 69e6644eb2 Fixing 3DH Auth Tests 2018-11-20 11:35:15 -08:00
Sarah Jamie Lewis 353ef38a54 Merge branch 'bine' of dan/libricochet-go into master 2018-11-20 19:19:34 +00:00
Dan Ballard 8fc60a0495 Mirating from bulb/asaur to bine, adding a generic Mixnet interface 2018-11-20 09:14:14 -08:00
Dan Ballard 880bd2e020 drone use proper path for test 2018-11-19 16:57:56 -08:00
Dan Ballard 493c2f5ec0 drone use proper path for repo 2018-11-19 15:14:58 -08:00
Sarah Jamie Lewis 18bc23d5c1 Merge branch 'test-fix' of dan/libricochet-go into master 2018-11-10 21:07:45 +00:00
Dan Ballard dfba540973 drone gogs notify more 2018-11-09 15:49:36 -08:00
Dan Ballard 3900552e06 testing script fail on fail; fix chatchannel test - update for 3dh 2018-11-09 15:47:21 -08:00
Sarah Jamie Lewis 49faf95ee7 Merge branch 'echobotv3' of openprivacy/libricochet-go into master 2018-10-27 17:37:02 +00:00
erinn 4d3f52102f allow retrieving handlers from an aif so we can merge them in cwtch peers 2018-10-27 02:17:35 -07:00
erinn ad9d0efb02 updating chatchannels and echobot to use v3 2018-10-25 19:20:58 -07:00
Dan Ballard 5b914aaf3a update drone to use new buildfiles/tor location 2018-10-10 15:42:55 -07:00
erinn 1abbb874cc Merge branch 'bugfix' of openprivacy/libricochet-go into master 2018-10-10 02:25:27 +00:00
erinn 8960be643c bugfix: the uncached connection attempt wasnt being returned 2018-10-09 19:24:22 -07:00
erinn 1056fce116 Merge branch 'asaur' of dan/libricochet-go into master 2018-10-09 23:21:39 +00:00
Dan Ballard 29e3a42cc9 asaur-ificate package and imports 2018-10-09 16:03:20 -07:00
Sarah Jamie Lewis bce6496829 Merge branch 'detports' of openprivacy/libricochet-go into master 2018-10-09 21:41:00 +00:00
erinn e825e52a7c check current onion descriptors on old versions of tor to see if they're out-of-sync 2018-10-09 12:55:42 -07:00
erinn fb8c0cac27 tidying up code paths and making detport selection a little better 2018-10-09 10:14:28 -07:00
erinn 5c98fd575b make local port selection deterministic and detach from the control port to improve performance 2018-10-08 20:19:19 -07:00
Sarah Jamie Lewis dd2af9f059 Merge branch 'bulbmigration' of openprivacy/libricochet-go into master 2018-10-09 01:54:13 +00:00
erinn 8a7895a359 oops, accidentally included a future change 2018-10-08 18:50:58 -07:00
erinn fd01dca056 forking bulb 2018-10-08 18:48:37 -07:00
Sarah Jamie Lewis 3ffe8ad0e4 Merge branch 'newnym' of openprivacy/libricochet-go into master 2018-10-08 16:50:01 +00:00
erinn aa7b398646 bugfix: actually attempt to connect again after a failed first attempt 2018-10-08 09:44:52 -07:00
Sarah Jamie Lewis 6c37867ed1 Merge branch 'newnym' of openprivacy/libricochet-go into master 2018-10-08 02:45:01 +00:00
erinn a6ca1571ad gofmt 2018-10-07 19:43:55 -07:00
erinn 06381579e5 after failing a new connection, flush the onion descriptor cache and try again in case the destination has cycled since its last use 2018-10-07 19:31:32 -07:00
Dan Ballard 530fa1f39f Merge branch 'v3onions' of openprivacy/libricochet-go into master 2018-10-05 20:24:04 +00:00
Sarah Jamie Lewis 5066380655 v3 onions 2018-10-05 13:06:54 -07:00
Dan Ballard 1667868d8c .drone: use openpriv version of drone-gogs 2018-07-17 12:06:55 -05:00
Sarah Jamie Lewis 18dcf5c2ae Merge branch 'drone-gogs' of dan/libricochet-go into master 2018-07-08 14:31:20 +00:00
Dan Ballard 90e39869a9 drone: move to drone-gogs 2018-07-07 20:16:43 -05:00
Sarah Jamie Lewis 930b94e0be Merge branch 'proto' of dan/libricochet-go into master 2018-07-05 21:54:47 +00:00
Dan Ballard b3c09e5409 ricochet protobuf files, for completeness 2018-07-05 16:49:52 -05:00
Sarah Jamie Lewis cc6076760f Merge branch 'drone' of dan/libricochet-go into master 2018-07-02 18:27:04 +00:00
Dan Ballard cdb458647f drone build file 2018-07-02 13:16:43 -05:00
Sarah Jamie Lewis 69c1dc18fb Merge branch 'master' of dan/libricochet-go into master 2018-06-25 16:19:58 +00:00
Dan Ballard e0411ecb9a fix: off by one error removing dead ricochet instances 2018-06-25 09:14:24 -07:00
Sarah Jamie Lewis c1907d5a9f Merge branch 'appinstance-cleanup' of dan/libricochet-go into master 2018-06-23 16:02:58 +00:00
Dan Ballard a852bc6678 application deletes application instance when it exit's process; adds CountConnections() 2018-06-23 08:57:22 -07:00
Sarah Jamie Lewis 2dc3739f56 Merge branch 'quality' of dan/libricochet-go into master 2018-06-23 15:47:56 +00:00
Dan Ballard 4442ab6691 code quality vet and lint script 2018-06-23 08:45:17 -07:00
Dan Ballard 7a1c1eb97e Merge branch 'rename' of openprivacy/libricochet-go into master 2018-06-09 16:08:40 +00:00
Sarah Jamie Lewis e382c8eb69 Rename 2018-06-08 15:05:22 -07:00
Dan Ballard 5a94afa0f7 add application integration test: (#42)
- start two peers alice, bob
- make alice requests contact with bob
- they send messages
- they shutdown
- verify (and fix) no threads leaked
- verify messages
- add inbound connection handler to chatChannelHandler
- add Open and AcceptAllContactHandler to application
2018-05-30 10:51:40 -07:00
Sarah Jamie Lewis 417d25dc7c BUGFIX: deadlock 2018-05-09 13:48:41 -07:00
Sarah Jamie Lewis 6f9718596d fmt and travis update 2018-05-09 12:40:07 -07:00
Sarah Jamie Lewis 9980da3bd5 Fixing channelmanager race condition, deleting vendoring 2018-05-09 12:06:34 -07:00
Dan Ballard 3920d33a77 add util to get onion address from private key (#39)
* add util to get onion address from private key

* PR comments: added test, using go-ricochet errors, function argument a privateKey

* remove unnecesary cast and variable
2018-04-30 18:03:06 -07:00
Sarah Jamie Lewis 92b9a0eb1f Commenting 2018-01-17 13:18:46 -05:00
Sarah Jamie Lewis 71685b9c3a Bugfix: Repeated channel opening attempts failed because err was shadowed 2018-01-16 12:13:46 -05:00
Sarah Jamie Lewis 9191b7530e Consolidating Channel Building / Teardown logic 2018-01-16 11:53:34 -05:00
Sarah Jamie Lewis 1e33c17ae3 Extract Authorization Logic / Refactoring OpenChannel Control Logic 2018-01-15 13:07:54 -05:00
Sarah Jamie Lewis 4994e54025 Identity Tests 2018-01-14 12:45:47 -05:00
Sarah Jamie Lewis f9209e187c Testing ContactChannel, Fixing CloseChannel Conditions 2018-01-14 12:16:14 -05:00
Sarah Jamie Lewis a04b3fe08b Moving ChannelResult processing to ControlChannel file and testing 2018-01-14 11:50:15 -05:00
Sarah Jamie Lewis cf46554922 Ensuring Channels Properly Close Themselves 2018-01-13 15:57:37 -05:00
Sarah Jamie Lewis 5d9f2ce9e3 Fixing identity tests and adding them to travis 2018-01-13 15:35:48 -05:00
Sarah Jamie Lewis 88d32191f7 Breaking out KeepAlive into a new control channel file for easier testing 2018-01-13 11:44:40 -05:00
Sarah Jamie Lewis b378c4c825 Revert "Protocol Question TODO"
This reverts commit 9788c07ac4.
2018-01-12 13:59:52 -05:00
Sarah Jamie Lewis 9788c07ac4 Protocol Question TODO 2018-01-12 13:56:32 -05:00
Sarah Jamie Lewis bf19d1b20c Moving FeaturesEnabled logic to another file, adding tests 2018-01-12 13:31:47 -05:00
Sarah Jamie Lewis 39cf4d3871 Removing go 1.7 from travis 2018-01-12 13:07:15 -05:00
Sarah Jamie Lewis 339995c101 Fixing gofmt 2018-01-12 13:04:20 -05:00
Sarah Jamie Lewis 4ccf95bee0 Adding RandNumber util to simplify chatchannel logic 2018-01-12 13:02:15 -05:00
Sarah Jamie Lewis 30808c71b2 Testing MultiChannel Response Condition in Channel Manage 2018-01-12 12:37:45 -05:00
Sarah Jamie Lewis 6e2bfbbc14 Application comments and notes 2018-01-09 17:31:54 -08:00
Sarah Jamie Lewis 4b700d4223 More golint fixing in utils 2018-01-08 11:02:04 -08:00
Sarah Jamie Lewis 1a2fb40d91 Refactoring Application to remove channel handler duplications
Also simplfies application a lot, still not complete, but i like this approach much more
2018-01-07 16:51:46 -08:00
Sarah Jamie Lewis be62634c46 Fixing go vet issues with errorf params 2018-01-06 16:37:07 -08:00
Sarah Jamie Lewis 05e8675ed5 Fixing a few golint issues 2018-01-05 14:16:52 -08:00
Sarah Jamie Lewis f6cc472c6e Removing Erroneous SendMessage & Adding SupportChannelTypes Test 2018-01-04 15:51:32 -08:00
Sarah Jamie Lewis 617ca019f8 Adding stub tests for MessageBuilder 2018-01-04 15:32:37 -08:00
Sarah Jamie Lewis 7f215e86c4 Adding Open Connection Failed Test 2018-01-03 10:20:53 -08:00
Sarah Jamie Lewis 84d7336602 Adding successful version negotiation tests.
This change also simplifies ricochet_test.go to only exercise the Open()
function.
2018-01-03 10:12:59 -08:00
Sarah Jamie Lewis 049a0ea15f Stubbing OutboundVersionNegotiationTest
Actually committing enable features work!
2018-01-02 09:23:20 -08:00
Sarah Jamie Lewis 6d449e230f Improving Testing Documentation & Coverage Starting With Inbound Version Negotiation 2018-01-01 10:55:59 -08:00
Sarah Jamie Lewis f537fb4f76 Adding Simple Application Broadcast & Features Enabled 2018-01-01 10:06:58 -08:00
Sarah Jamie Lewis 1433b31e6f Change inbound/outbound handlers to use Identity.
Add Inbound Version Negotiation Test
2017-12-13 11:42:54 -08:00
Sarah Jamie Lewis 43b357fdb6 First cut of minimizing private_key exposure in the code base
Minor formatting
2017-12-05 11:00:04 -08:00
Sarah Jamie Lewis e1031861a2 Adding basic tests for crypto_utils 2017-11-04 13:31:37 -07:00
Sarah Jamie Lewis 958e07bf66 Fixing minor govet / misspell issue 2017-11-04 08:56:20 -07:00
Sarah Jamie Lewis 5057dd68ee Formatting + Checking Connection Error in Echobot 2017-11-02 16:45:27 -07:00
Sarah Jamie Lewis 9d2e898157
Update README.md
Fixing the examples & adding removing untrue statements.
2017-11-02 16:15:12 -07:00
Sarah Jamie Lewis 3e6dc80670 Fixup Application to align with new Connection API 2017-11-02 16:05:01 -07:00
Sarah Jamie Lewis dc285b18a9 Merge branch 'master' of https://github.com/dballard/go-ricochet into dballard-master 2017-11-02 15:53:01 -07:00
Sarah Jamie Lewis 8fe7b84fc9 Merge branch 'fix/chatchannel-api' of https://github.com/special/go-ricochet-protocol into special-fix/chatchannel-api 2017-11-02 15:45:09 -07:00
John Brooks 9a65aeed77 Improve ContactRequestChannel's API
After the RequestOpenChannel changes, it's now possible to specify the
name and message of an outbound request as variables of the channel handler,
instead implementing an interface method to return them.

Also added the SendResponse method, which is necessary to respond to an
inbound request that was in the Pending state.
2017-11-02 15:43:07 -07:00
John Brooks b2c87b1b72 Fix concurrency issues in ProcessAuthAsClient/Server
There were a few related issues with ProcessAuthAsClient/Server that
could cause deadlocks or leak goroutines:

Break() could end up being called more than once, which is always a
deadlock. This is fixed by using a sync.Once.

RequestOpenChannel was called without Do, and was called before
Process in the same goroutine, which would have deadlocked if Do was
used.

Timing out the authentication attempt wouldn't directly abort
Process(); it would only exit if the connection was closed somewhere
else.

It may be a good idea to change some of this to guarantee that
ProcessAuthAsClient returns either an authenticated connection or closes
the connection, but I'll leave that as a separate task for the moment.
2017-11-02 15:41:35 -07:00
John Brooks e459a56286 Prevent deadlocks with Do or Break and closing connections
After the last round of fixes in Do, there was still one major issue
making it impossible to use Do or Break safely: closing connections.

Connections can be closed spontaneously, and this causes Process to
return. At that moment any ongoing call to Do or Break has deadlocked;
nothing will ever read those channels again. To prevent this, we have to
ensure that Do or Break won't try to send to Process' channels once it
has stopped reading from them. Doing that without other races is tricky.

The solution here, briefly, is to acquire a mutex in Do and Break before
checking the `closed` boolean, and hold that mutex for the entire
operation (including any blocking channel operations). When Process is
closing down the connection, it uses a separate goroutine to acquire the
same mutex and change the boolean, while still handling channel reads.
Once the boolean has changed, the mutex guarantees that nothing will try
to send to these channels again.

I've tried to document the problems and solutions in the code, because
it is subtle in some places and this is definitely critical code.
2017-11-02 15:41:34 -07:00
John Brooks c24773809e Fix and document safety problems with Connection.Do
There were several issues with the Do function that made it nearly
impossible to write safe code.

First, Do cannot be called recursively -- it will deadlock. There is
actually no way to implement a safe and recursive Do (or mutex) in Go,
because there is no primitive that will identify the current goroutine.

RequestOpenChannel used Do internally, which made it impossible to open
channels safely in many circumstances. That has been removed, so all
calls to RequestOpenChannel must be changed to happen under Do now.

Do now has more documentation and a new rule: no code exposed through
API can use Do, unless it has sole custody of the connection (such as
ProcessAuthAsClient).

Related to that problem, Do was impossible to call from inside handlers
(or anything else on the process goroutine) -- it would again just
deadlock. This is resolved by wrapping calls into user code to continue
handling invocations of Do (and only those) while the handler is
executing.

There is a third issue with connection close, but it will be addressed
in a separate commit

And finally, because it's impossible to timeout or interrupt a call to
Do, I also added a DoContext method that takes a go Context, which is
also passed through to the called function.
2017-11-02 15:41:30 -07:00
John Brooks 0f47f62465 Return the new channel from RequestOpenChannel
This fixes a quirk where it would've been difficult to tell which of
several channels of the same type+direction is the one you just created.

It's also a fairly common pattern to want to interact with a channel
right after opening it; for example, a chat channel is opened and can
immediately send messages before getting the peer response. It's
convenient to not have to do a separate lookup.
2017-11-02 15:40:02 -07:00
John Brooks d19102b257 Pass channel handler directly to RequestOpenChannel
RequestOpenChannel is the primary API to open a new outbound channel. It
was written to take a connection.Handler and use OnOpenChannelRequest
to get a channels.Handler to represent the new channel, which is the
same path that inbound channels will take.

Going through the global OnOpenChannelRequest method makes this much
less flexible and prevents passing parameters to the new channel handler
during creation.  This also requires users of the API to know/find the
connection handler, or worse, to boilerplate one into existence for their
channel creation.

Instead, I think this function should take a channels.Handler directly,
so that the caller gets full control over the handler for their new
channel.

As part of that change, I've also moved the authentication logic in
AutoConnectionHandler to be contained entirely within
{In,Out}boundConnectionHandler.
2017-11-02 15:40:02 -07:00
Sarah Jamie Lewis 1ed9265866
Merge pull request #31 from special/fix/pointers-to-interfaces
Don't use pointers to interfaces
2017-11-02 14:32:55 -07:00
Sarah Jamie Lewis ec16eee2aa
Merge pull request #29 from special/fix/create-outbound-conn
Make NegotiateVersionOutbound a public function
2017-11-02 14:31:42 -07:00
John Brooks a62d1bbcc9 Improve ChatChannel API for message acknowledgement
ChatChannel didn't return the message ID for sent messages, which made
using the returned ACKs impossible. The SendMessage method now returns
the uin32 messageID.

Also, SendMessage didn't support the TimeDelta field for messages, which
is used for queued or resent messages. This is now available as
SendMessageWithTime.

And finally, the ChatMessageAck callback didn't indicate if mesasges
were accepted or not, which is part of the protocol. That was added as a
field, which is unfortunately a breaking API change, but I've made
enough of those lately to not feel guilty about it.
2017-09-25 13:04:21 -07:00
John Brooks ea788d58ef Don't use pointers to interfaces
There are few situations where a pointer to an interface is useful in
Go, and this isn't one. Interfaces can hold types by value or pointer,
so long as that type fulfills the interface.
2017-09-23 16:44:12 -06:00
John Brooks 41d9401ca4 Make NegotiateVersionOutbound a public function
This is needed to allow creating a Connection for an arbitrary
application-provided io.ReadWriteCloser, instead of doing network logic
inside of go-ricochet. ricochet-go does its own connection management.

I would rather rework this API so that Connection has two construct
methods, inbound and outbound, that do version negotiation and are
always used. The methods that do networking and construct a Connection
would then be a separate (and non-root) package building on top of that.
2017-09-19 14:19:06 -06:00
Dan Ballard f04239c885 make echobot use new SetupOnion 2017-09-04 20:33:21 -07:00
Sarah Jamie Lewis d2dceef028 Merge pull request #26 from dballard/crypto-utils
Add more crypto/utils and extend SetupOnion to support unix sockets
2017-08-15 11:34:26 -07:00
Dan Ballard 5937ceee73 Add more crypto/utils and extend SetupOnion to support unix sockets 2017-08-14 08:43:33 -07:00
Sarah Jamie Lewis 93baafc2f7 Adding bulb to godep 2017-08-05 12:48:25 -07:00
Sarah Jamie Lewis 22cbf5d738 First Cut of Applications + Bugs, Formatting 2017-07-04 11:29:11 -07:00
Sarah Jamie Lewis 1cf7c2b7c7 Adding a Trace log to Connection and removing all other logging directives 2017-06-27 12:48:35 -07:00
Sarah Jamie Lewis f4ed1c244b Adding Inbound Version Negotiation
+ Error handling for missing private key setting
2017-06-27 10:39:33 -07:00
Sarah Jamie Lewis a2fa40492a Merge pull request #22 from dballard/patch-1
fix typo
2017-06-10 20:18:11 -07:00
Sarah Jamie Lewis 4f1a2f82cc Merge pull request #21 from dballard/new_api-fix_app
fix syntax errors
2017-06-10 20:17:58 -07:00
Dan Ballard d895b46a03 fix typo 2017-06-10 16:19:56 -07:00
Dan Ballard 6f07cff0bc fix syntax errors 2017-06-10 15:20:41 -07:00
Sarah Jamie Lewis 5d767174b1 Brand new API v0.2 2017-05-02 16:33:51 -07:00
Sarah Jamie Lewis 5a720a08d0 Merge pull request #17 from special/api-handlers
Rework the API around connection events
2017-01-15 15:49:09 -08:00
John Brooks 860ae9a024 Rework the API around connection events
This is a rework of some parts of the API to make connection management
for applications more sane and reliable.

- The RicochetService interface is split into the ServiceHandler and
  ConnectionHandler interfaces. ServiceHandler is implemented by the
  application to handle inbound connections to a listener.
  ConnectionHandler is implemented to handle events for a single
  OpenConnection. Handler instances should no longer be shared for
  different listeners or connections.

- Instead of automatically starting a processConnection goroutine, the
  application is now responsible for calling OpenConnection.Process in a
  goroutine to act on the connection. This function blocks until the
  connection is closed. This change allows a better application pattern
  for setting the handler of a connection and reacting to connection
  loss.

- It is no longer necessary to have started a listener in order to make
  outbound connections.

- The Ricochet type is removed, because it no longer served any purpose,
  and this avoids having any shared state between different listeners or
  connections.
2016-12-03 16:53:13 -08:00
91 changed files with 5863 additions and 1588 deletions

51
.drone.yml Normal file
View File

@ -0,0 +1,51 @@
workspace:
base: /go
path: src/git.openprivacy.ca/openprivacy/libricochet-go
pipeline:
fetch:
image: golang
environment:
- GO111MODULE=on
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- go get -u golang.org/x/lint/golint
- go mod download
quality:
image: golang
environment:
- GO111MODULE=on
commands:
- go list ./... | xargs go vet
- go list ./... | grep -v "/wire/" | xargs golint -set_exit_status
units-tests:
image: golang
environment:
- GO111MODULE=on
commands:
- sh testing/tests.sh
integ-test:
image: golang
environment:
- GO111MODULE=on
commands:
- ./tor -f ./torrc
- sleep 15
- go test -race -v git.openprivacy.ca/openprivacy/libricochet-go/testing
notify-email:
image: drillster/drone-email
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
status: [ failure ]
notify-gogs:
image: openpriv/drone-gogs
when:
event: pull_request
status: [ success, changed, failure ]
secrets: [gogs_account_token]
gogs_url: https://git.openprivacy.ca

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
go-ricochet-coverage.out
*~
*.out
.idea
.reviewboardrc
/vendor/
/testing/tor/
/connectivity/tor/
/tor/

View File

@ -1,6 +1,5 @@
language: go
go:
- 1.7
- tip
sudo: true
notifications:
@ -13,9 +12,10 @@ install:
- go get github.com/mattn/goveralls
- go get golang.org/x/net/proxy
- go get github.com/golang/protobuf/proto
- go get github.com/yawning/bulb/
script:
- cd $TRAVIS_BUILD_DIR && ./tests.sh
- cd $TRAVIS_BUILD_DIR && ./testing/tests.sh
- test -z "$GOFMT"
- goveralls -coverprofile=./coverage.out -service travis-ci

View File

@ -33,13 +33,12 @@ can check coverage `with go test --cover github.com/s-rah/go-ricochet`
Format your code (the path might be slightly different):
* `gofmt -l=true -s -w src/github.com/s-rah/go-ricochet/`
* `gofmt -l=true -s -w src/git.openprivacy.ca/openprivacy/libricochet-go/`
Run the following commands, and address any issues which arise:
* `go vet github.com/s-rah/go-ricochet/...`
* `golint github.com/s-rah/go-ricochet`
* `golint github.com/s-rah/go-ricochet/utils`
* `go vet git.openprivacy.ca/openprivacy/libricochet-go/...`
* `/golint application/ channels/ connection/ examples/ identity/ policies/ testing/ utils`
## 4. Code Review

12
LICENSE
View File

@ -25,8 +25,8 @@ SOFTWARE.
--------------------------------------------------------------------------------
Autogenerated protobuf code was generated using the proto file from Ricochet.
They are covered under the following license.
Autogenerated protobuf code was generated using the proto files from Ricochet.
They are covered under the following license.
Ricochet - https://ricochet.im/
Copyright (C) 2014, John Brooks <john.brooks@dereferenced.net>
@ -61,10 +61,4 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
The go-ricochet logo is based on an image by Olga Shalakhina
<osshalakhina@gmail.com> who in turn modified the original gopher images made by
Renee French. The image is licensed under Creative Commons 3.0 Attributions.
--------------------------------------------------------------------------------
go-ricochet is not affiliated with or endorsed by Ricochet.im or the Tor Project.
libricochet-go is not affiliated with or endorsed by Ricochet.im or the Tor Project.

View File

@ -1,63 +1,18 @@
# GoRicochet [![Build Status](https://travis-ci.org/s-rah/go-ricochet.svg?branch=master)](https://travis-ci.org/s-rah/go-ricochet) [![Go Report Card](https://goreportcard.com/badge/github.com/s-rah/go-ricochet)](https://goreportcard.com/report/github.com/s-rah/go-ricochet) [![Coverage Status](https://coveralls.io/repos/github/s-rah/go-ricochet/badge.svg?branch=master)](https://coveralls.io/github/s-rah/go-ricochet?branch=master)
# libricochet-go [![Go Report Card](https://goreportcard.com/badge/git.openprivacy.ca/openprivacy/libricochet-go)](https://goreportcard.com/report/git.openprivacy.ca/openprivacy/libricochet-go)
![GoRicochet](logo.png)
libricochet-go is an experimental implementation of the [Ricochet Protocol](https://ricochet.im)
written in Go.
GoRicochet is an experimental implementation of the [Ricochet Protocol](https://ricochet.im)
in Go.
## Differences to Ricochet IM
## Features
* *V3 Onion Support* - libricochet-go updates the Ricochet protocol to use V3 tor onion service addresses, and implements a new authentication protocol providing greater deniability.
* *Library* - libricochet-go is designed to be integrated within your application, allowing your application to communicate with other peers or programs in a way that is privacy preserving and metadata resistant.
* A simple API that you can use to build Automated Ricochet Applications
* A suite of regression tests that test protocol compliance.
## Using libricochet-go
## Building an Automated Ricochet Application
Below is a simple echo bot, which responds to any chat message. You can also find this code under `examples/echobot`
package main
import (
"github.com/s-rah/go-ricochet"
"log"
)
type EchoBotService struct {
goricochet.StandardRicochetService
}
// Always Accept Contact Requests
func (ts *EchoBotService) IsKnownContact(hostname string) bool {
return true
}
func (ts *EchoBotService) OnContactRequest(oc *goricochet.OpenConnection, channelID int32, nick string, message string) {
ts.StandardRicochetService.OnContactRequest(oc, channelID, nick, message)
oc.AckContactRequestOnResponse(channelID, "Accepted")
oc.CloseChannel(channelID)
}
func (ebs *EchoBotService) OnChatMessage(oc *goricochet.OpenConnection, channelID int32, messageId int32, message string) {
log.Printf("Received Message from %s: %s", oc.OtherHostname, message)
oc.AckChatMessage(channelID, messageId)
if oc.GetChannelType(6) == "none" {
oc.OpenChatChannel(6)
}
oc.SendMessage(6, message)
}
func main() {
ricochetService := new(EchoBotService)
ricochetService.Init("./private_key")
ricochetService.Listen(ricochetService, 12345)
}
Each automated ricochet service can extend of the `StandardRicochetService`. From there
certain functions can be extended to fully build out a complete application.
Currently GoRicochet does not establish a hidden service, so to make this service
available to the world you will have to [set up a hidden service](https://www.torproject.org/docs/tor-hidden-service.html.en)
Checkout our [EchoBot Example](https://git.openprivacy.ca/openprivacy/libricochet-go/src/master/application/examples/echobot) to get started.
## Security and Usage Note
This project is experimental and has not been independently reviewed. If you are
looking for a quick and easy way to use ricochet please check out [Ricochet Protocol](https://ricochet.im).
looking for a quick and easy way to use ricochet please check out [Ricochet IM](https://ricochet.im).

View File

@ -0,0 +1,22 @@
package application
// AcceptAllContactHandler is a pass through Contact Handler. It is currently only used by the integration test.
// TODO: DEPRECATE
type AcceptAllContactHandler struct{}
// ContactRequest returns "Pending" for everything
func (aach *AcceptAllContactHandler) ContactRequest(name string, message string) string {
return "Pending"
}
// ContactRequestRejected is a noop
func (aach *AcceptAllContactHandler) ContactRequestRejected() {
}
// ContactRequestAccepted is a noop
func (aach *AcceptAllContactHandler) ContactRequestAccepted() {
}
// ContactRequestError is a noop
func (aach *AcceptAllContactHandler) ContactRequestError() {
}

View File

@ -0,0 +1,28 @@
package application
import (
"crypto/rsa"
"golang.org/x/crypto/ed25519"
)
// AcceptAllContactManager implements the contact manager interface an presumes
// all connections are allowed.
// It is currently used by the Cwtch Server.
// TODO Deprecate
type AcceptAllContactManager struct {
}
// LookupContact returns that a contact is known and allowed to communicate for all cases.
func (aacm *AcceptAllContactManager) LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
return true, true
}
// LookupContactV3 returns that a contact is known and allowed to communicate for all cases.
func (aacm *AcceptAllContactManager) LookupContactV3(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) {
return true, true
}
// ContactRequest accepts every single Contact Request
func (aacm *AcceptAllContactManager) ContactRequest(name string, message string) string {
return "Accepted"
}

159
application/application.go Normal file
View File

@ -0,0 +1,159 @@
package application
import (
"git.openprivacy.ca/openprivacy/libricochet-go"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"net"
"sync"
)
const (
// RicochetPort is the default port used by ricochet applications
RicochetPort = 9878
)
// RicochetApplication bundles many useful constructs that are
// likely standard in a ricochet application
type RicochetApplication struct {
contactManager ContactManagerInterface
v3identity identity.Identity
name string
ls connectivity.ListenService
acn connectivity.ACN
instances []*Instance
lock sync.Mutex
aif InstanceFactory
}
// Init initializes the underlying RicochetApplication datastructure, making it ready for use
func (ra *RicochetApplication) Init(acn connectivity.ACN, name string, v3identity identity.Identity, af InstanceFactory, cm ContactManagerInterface) {
ra.acn = acn
ra.name = name
ra.v3identity = v3identity
ra.aif = af
ra.contactManager = cm
}
// TODO: Reimplement OnJoin, OnLeave Events.
func (ra *RicochetApplication) handleConnection(conn net.Conn) {
rc, err := goricochet.NegotiateVersionInbound(conn)
if err != nil {
log.Errorln("There was an error")
conn.Close()
return
}
ich := connection.HandleInboundConnection(rc)
err = ich.ProcessAuthAsV3Server(ra.v3identity, ra.contactManager.LookupContactV3)
if err != nil {
log.Errorf("There was an error authenticating the connection: %v", err)
conn.Close()
return
}
rai := ra.aif.GetApplicationInstance(rc)
ra.lock.Lock()
ra.instances = append(ra.instances, rai)
ra.lock.Unlock()
rc.Process(rai)
// rc.Process ends when the connection ends.
// Remove it from the application's list of instances
ra.lock.Lock()
for i, x := range ra.instances {
if x == rai {
ra.instances = append(ra.instances[:i], ra.instances[i+1:]...)
break
}
}
ra.lock.Unlock()
}
// HandleApplicationInstance delegates handling of a given Instance to the Application.
func (ra *RicochetApplication) HandleApplicationInstance(rai *Instance) {
ra.lock.Lock()
ra.instances = append(ra.instances, rai)
ra.lock.Unlock()
}
// Open a connection to another Ricochet peer at onionAddress. Infof they are unknown to use, use requestMessage (otherwise can be blank)
func (ra *RicochetApplication) Open(onionAddress string, requestMessage string) (*Instance, error) {
rc, err := goricochet.Open(ra.acn, onionAddress)
if err != nil {
log.Errorf("Error in application.Open(): %v\n", err)
return nil, err
}
och := connection.HandleOutboundConnection(rc)
_, err = och.ProcessAuthAsV3Client(ra.v3identity)
if err != nil {
log.Errorf("There was an error authenticating the connection: %v", err)
return nil, err
}
rai := ra.aif.GetApplicationInstance(rc)
go rc.Process(rai)
ra.HandleApplicationInstance(rai)
return rai, nil
}
// Broadcast performs the given function do() over all application instance (all connected peers)
func (ra *RicochetApplication) Broadcast(do func(rai *Instance)) {
ra.lock.Lock()
for _, rai := range ra.instances {
do(rai)
}
ra.lock.Unlock()
}
// Shutdown stops a RicochetApplication, terminating all child processes and resources
func (ra *RicochetApplication) Shutdown() {
ra.lock.Lock()
ra.ls.Close()
for _, instance := range ra.instances {
instance.Connection.Close()
}
ra.lock.Unlock()
}
// Close kills a connection by a given Onion Address
func (ra *RicochetApplication) Close(onion string) {
ra.lock.Lock()
for _, instance := range ra.instances {
if instance.RemoteHostname == onion {
instance.Connection.Close()
}
}
ra.lock.Unlock()
}
// ConnectionCount returns the number of concurrent connections to the application
func (ra *RicochetApplication) ConnectionCount() int {
return len(ra.instances)
}
// Run handles a Listen object and Accepts and handles new connections
func (ra *RicochetApplication) Run(ls connectivity.ListenService) {
if !ra.v3identity.Initialized() || ra.contactManager == nil {
return
}
ra.lock.Lock()
ra.ls = ls
ra.lock.Unlock()
var err error
for err == nil {
conn, err := ra.ls.Accept()
if err == nil {
go ra.handleConnection(conn)
} else {
return
}
}
}

View File

@ -0,0 +1,58 @@
package application
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
)
// Instance is a concrete instance of a ricochet application, encapsulating a connection
type Instance struct {
connection.AutoConnectionHandler
Connection *connection.Connection
RemoteHostname string
}
// InstanceFactory generates ApplicationInstances on a specific connection.
type InstanceFactory struct {
handlerMap map[string]func(*Instance) func() channels.Handler
}
// Init sets up an Application Factory
func (af *InstanceFactory) Init() {
af.handlerMap = make(map[string]func(*Instance) func() channels.Handler)
}
// AddHandler defines a channel type -> handler construct function
func (af *InstanceFactory) AddHandler(ctype string, chandler func(*Instance) func() channels.Handler) {
af.handlerMap[ctype] = chandler
}
// GetHandlers returns all handlers
func (af *InstanceFactory) GetHandlers() []string {
keys := make([]string, len(af.handlerMap))
i := 0
for k := range af.handlerMap {
keys[i] = k
i++
}
return keys
}
// GetHandler returns a set handler for the channel type.
func (af *InstanceFactory) GetHandler(ctype string) func(*Instance) func() channels.Handler {
return af.handlerMap[ctype]
}
// GetApplicationInstance builds a new application instance using a connection as a base.
func (af *InstanceFactory) GetApplicationInstance(rc *connection.Connection) *Instance {
rai := new(Instance)
rai.Init()
rai.RemoteHostname = rc.RemoteHostname
rai.Connection = rc
for t, h := range af.handlerMap {
rai.RegisterChannelHandler(t, h(rai))
}
return rai
}

View File

@ -0,0 +1,13 @@
package application
import (
"crypto/rsa"
"golang.org/x/crypto/ed25519"
)
// ContactManagerInterface provides a mechanism for autonous applications
// to make decisions on what connections to accept or reject.
type ContactManagerInterface interface {
LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool)
LookupContactV3(hostname string, publicKey ed25519.PublicKey) (allowed, known bool)
}

View File

@ -0,0 +1,112 @@
package alicebot
import (
"crypto/rand"
"git.openprivacy.ca/openprivacy/libricochet-go"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/ed25519"
"os"
"time"
)
// NewAliceBot creates a new AliceBot and establishes a connection to the given onion server.
func NewAliceBot(acn connectivity.ACN, onion string) AliceBot {
alice := new(alicebot)
alice.messages = make(map[uint32]string)
var err error
alice.pub, alice.priv, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Errorf("[alice] error generating key: %v", err)
os.Exit(1)
}
rc, err := goricochet.Open(acn, onion)
if err != nil {
log.Errorf("[alice] error connecting to echobot: %v", err)
os.Exit(1)
}
_, err = connection.HandleOutboundConnection(rc).ProcessAuthAsV3Client(identity.InitializeV3("alice", &alice.priv, &alice.pub))
if err != nil {
log.Errorf("[alice] failed to authenticate connection: %v", err)
os.Exit(1)
}
alice.rc = rc
ach := connection.AutoConnectionHandler{}
ach.Init()
ach.RegisterChannelHandler("im.ricochet.chat", func() channels.Handler {
chat := new(channels.ChatChannel)
chat.Handler = alice
return chat
})
go alice.rc.Process(&ach)
log.Infof("[alice] requesting channel...")
alice.rc.Do(func() error {
chatchannel := channels.ChatChannel{}
chatchannel.Handler = alice
_, err := alice.rc.RequestOpenChannel("im.ricochet.chat", &chatchannel)
if err != nil {
log.Errorf("failed requestopenchannel: %v", err)
os.Exit(1)
}
return nil
})
return alice
}
type alicebot struct {
messages map[uint32]string
pub ed25519.PublicKey
priv ed25519.PrivateKey
mID int
rc *connection.Connection
}
// AliceBot is an interface for alicebot, allowing callers to send and receive messages.
type AliceBot interface {
channels.ChatChannelHandler
SendMessage(string)
}
// SendMessage can be called to send a message to EchoBot
func (ab *alicebot) SendMessage(message string) {
// The following code opens (or creates) a new im.ricochet.chat channel to the connected service
// and sends a message.
log.Infof("[alice] sending...")
ab.rc.Do(func() error {
channel := ab.rc.Channel("im.ricochet.chat", channels.Outbound)
id, err := channels.SendMessageOnChatChannel(channel, message)
if err == nil {
ab.messages[id] = message
}
return err
})
}
// OpenInbound is called when EchoBot attempts to open a channel with AliceBot
func (ab *alicebot) OpenInbound() {
log.Infof("[alice] inbound connection established")
}
// ChatMessage is called whenever AliceBot receives a message from EchoBot
func (ab *alicebot) ChatMessage(messageID uint32, when time.Time, message string) bool {
log.Infof("[alice] got message from echobot: %s", message)
return true
}
// ChatMessageAck is called whenever AliceBot received an acknowledgement of a previously sent message.
func (ab *alicebot) ChatMessageAck(messageID uint32, accepted bool) {
log.Infof("[alice] message \"%s\" ack'd", ab.messages[messageID])
}

View File

@ -0,0 +1,132 @@
package main
import (
"crypto/rand"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/application/examples/echobot/alicebot"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"time"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"golang.org/x/crypto/ed25519"
"os"
)
// EchoBotInstance is an Instance of the EchoBot Application. One is created for every connected peer.
type EchoBotInstance struct {
rai *application.Instance
ra *application.RicochetApplication
}
// Init establishes an EchoBotInstance
func (ebi *EchoBotInstance) Init(rai *application.Instance, ra *application.RicochetApplication) {
ebi.rai = rai
ebi.ra = ra
}
// OpenInbound is called when AliceBot opens a ChatChannel. In this case, because we want EchoBot to respond we
// need to open a new channel in the other direction.
func (ebi *EchoBotInstance) OpenInbound() {
log.Debugln("OpenInbound() ChatChannel handler called...")
outboutChatChannel := ebi.rai.Connection.Channel("im.ricochet.chat", channels.Outbound)
if outboutChatChannel == nil {
ebi.rai.Connection.Do(func() error {
ebi.rai.Connection.RequestOpenChannel("im.ricochet.chat",
&channels.ChatChannel{
Handler: ebi,
})
return nil
})
}
}
// ChatMessage is called whenever a connected peer sends a message to EchoBot
func (ebi *EchoBotInstance) ChatMessage(messageID uint32, when time.Time, message string) bool {
log.Infof("message from %v - %v", ebi.rai.RemoteHostname, message)
ebi.SendChatMessage(ebi.rai, ebi.rai.RemoteHostname+" "+message)
return true
}
// ChatMessageAck is called whenever a connected peer acknowledges a message that EchoBot sent.
func (ebi *EchoBotInstance) ChatMessageAck(messageID uint32, accepted bool) {
}
// SendChatMessage sends a chat message to the given echobot instance
func (ebi *EchoBotInstance) SendChatMessage(rai *application.Instance, message string) {
ebi.rai.Connection.Do(func() error {
channel := ebi.rai.Connection.Channel("im.ricochet.chat", channels.Outbound)
// We are swallowing the message id and the error here, in reality you will want to handle it.
channels.SendMessageOnChatChannel(channel, message)
return nil
})
}
// main() encapsulates an entire ricochet ecosystem, from starting tor, to generating onion service keys to managing
// the launching of both the echobot server and the alicebot peer.
// In most systems you will only be handling one or two of these subsystems at any given time so this example might seem
// bloated, but we have tried to highlight the most interesting aspects to allow easy application to new domains.
func main() {
// Set up Logging.
log.SetLevel(log.LevelInfo)
log.AddEverythingFromPattern("connectivity")
// Set up Tor
acn, err := connectivity.StartTor(".", "")
if err != nil {
log.Errorf("Unable to start Tor: %v", err)
os.Exit(1)
}
defer acn.Close()
// Set up the Echobot Server
echobot := new(application.RicochetApplication)
cpubk, cprivk, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Errorf("Error generating keys: %v", err)
os.Exit(1)
}
// Turn on the echobot onion service in Tor.
listenService, err := acn.Listen(cprivk, application.RicochetPort)
if err != nil {
log.Errorf("error setting up onion service: %v", err)
os.Exit(1)
}
// This next section looks complicated (and it is a little), but all it is doing is allowing echobot to handle
// im.ricochet.chat type channels.
af := application.InstanceFactory{}
af.Init()
af.AddHandler("im.ricochet.chat", func(rai *application.Instance) func() channels.Handler {
ebi := new(EchoBotInstance)
ebi.Init(rai, echobot)
return func() channels.Handler {
chat := new(channels.ChatChannel)
chat.Handler = ebi
return chat
}
})
// Thee next few lines turn on echobot and make it available to listen to new connections.
// Note that we initialize a V3 identity for echobot.
echobot.Init(acn, "echobot", identity.InitializeV3("echobot", &cprivk, &cpubk), af, new(application.AcceptAllContactManager))
log.Infof("echobot listening on %v", listenService.AddressFull())
go echobot.Run(listenService)
// Now we wait a little bit for everything to wire itself together.
log.Infoln("counting to five ...")
time.Sleep(time.Second * 5)
// Finally, in these last few lines we setup an AliceBot who simply sends messages to echobot
alice := alicebot.NewAliceBot(acn, listenService.AddressIdentity())
alice.SendMessage("be gay")
alice.SendMessage("do crime")
// stick around and see what happens
time.Sleep(time.Second * 30)
}

View File

@ -1,57 +0,0 @@
package goricochet
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"io"
)
// AuthenticationHandler manages the state required for the AuthHiddenService
// authentication scheme for ricochet.
type AuthenticationHandler struct {
clientCookie [16]byte
serverCookie [16]byte
}
// AddClientCookie adds a client cookie to the state.
func (ah *AuthenticationHandler) AddClientCookie(cookie []byte) {
copy(ah.clientCookie[:], cookie[:16])
}
// AddServerCookie adds a server cookie to the state.
func (ah *AuthenticationHandler) AddServerCookie(cookie []byte) {
copy(ah.serverCookie[:], cookie[:16])
}
// GenRandom generates a random 16byte cookie string.
func (ah *AuthenticationHandler) GenRandom() [16]byte {
var cookie [16]byte
io.ReadFull(rand.Reader, cookie[:])
return cookie
}
// GenClientCookie generates and adds a client cookie to the state.
func (ah *AuthenticationHandler) GenClientCookie() [16]byte {
ah.clientCookie = ah.GenRandom()
return ah.clientCookie
}
// GenServerCookie generates and adds a server cookie to the state.
func (ah *AuthenticationHandler) GenServerCookie() [16]byte {
ah.serverCookie = ah.GenRandom()
return ah.serverCookie
}
// GenChallenge constructs the challenge parameter for the AuthHiddenService session.
// The challenge is the a Sha256HMAC(clientHostname+serverHostname, key=clientCookie+serverCookie)
func (ah *AuthenticationHandler) GenChallenge(clientHostname string, serverHostname string) []byte {
key := make([]byte, 32)
copy(key[0:16], ah.clientCookie[:])
copy(key[16:], ah.serverCookie[:])
value := []byte(clientHostname + serverHostname)
mac := hmac.New(sha256.New, key)
mac.Write(value)
hmac := mac.Sum(nil)
return hmac
}

View File

@ -1,32 +0,0 @@
package goricochet
import "testing"
import "bytes"
func TestGenChallenge(t *testing.T) {
authHandler := new(AuthenticationHandler)
authHandler.AddClientCookie([]byte("abcdefghijklmnop"))
authHandler.AddServerCookie([]byte("qrstuvwxyz012345"))
challenge := authHandler.GenChallenge("test.onion", "notareal.onion")
expectedChallenge := []byte{0xf5, 0xdb, 0xfd, 0xf0, 0x3d, 0x94, 0x14, 0xf1, 0x4b, 0x37, 0x93, 0xe2, 0xa5, 0x11, 0x4a, 0x98, 0x31, 0x90, 0xea, 0xb8, 0x95, 0x7a, 0x2e, 0xaa, 0xd0, 0xd2, 0x0c, 0x74, 0x95, 0xba, 0xab, 0x73}
t.Log(challenge, expectedChallenge)
if bytes.Compare(challenge[:], expectedChallenge[:]) != 0 {
t.Errorf("AuthenticationHandler Challenge Is Invalid, Got %x, Expected %x", challenge, expectedChallenge)
}
}
func TestGenClientCookie(t *testing.T) {
authHandler := new(AuthenticationHandler)
clientCookie := authHandler.GenClientCookie()
if clientCookie != authHandler.clientCookie {
t.Errorf("AuthenticationHandler Client Cookies are Different %x %x", clientCookie, authHandler.clientCookie)
}
}
func TestGenServerCookie(t *testing.T) {
authHandler := new(AuthenticationHandler)
serverCookie := authHandler.GenServerCookie()
if serverCookie != authHandler.serverCookie {
t.Errorf("AuthenticationHandler Server Cookies are Different %x %x", serverCookie, authHandler.serverCookie)
}
}

29
channels/channel.go Normal file
View File

@ -0,0 +1,29 @@
package channels
// Direction indicated whether we or the remote peer opened the channel
type Direction int
const (
// Inbound indcates the channel was opened by the remote peer
Inbound Direction = iota
// Outbound indicated the channel was opened by us
Outbound
)
// Channel holds the state of a channel on an open connection
type Channel struct {
ID int32
Type string
Direction Direction
Handler Handler
Pending bool
ServerHostname string
ClientHostname string
// Functions for updating the underlying Connection
SendMessage func([]byte)
CloseChannel func()
DelegateAuthorization func()
DelegateEncryption func([32]byte)
}

165
channels/chatchannel.go Normal file
View File

@ -0,0 +1,165 @@
package channels
import (
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/chat"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"time"
)
// ChatChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.chat". The channel may be inbound or outbound.
//
// ChatChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge chat messages. The application must provide
// a ChatChannelHandler implementation to handle chat events.
type ChatChannel struct {
// Methods of Handler are called for chat events on this channel
Handler ChatChannelHandler
channel *Channel
lastMessageID uint32
}
// ChatChannelHandler is implemented by an application type to receive
// events from a ChatChannel.
//
// Note that ChatChannelHandler is composable with other interfaces, including
// ConnectionHandler; there is no need to use a distinct type as a
// ChatChannelHandler.
type ChatChannelHandler interface {
// OpenInbound is called when a inbound chat channel is opened
OpenInbound()
// ChatMessage is called when a chat message is received. Return true to acknowledge
// the message successfully, and false to NACK and refuse the message.
ChatMessage(messageID uint32, when time.Time, message string) bool
// ChatMessageAck is called when an acknowledgement of a sent message is received.
ChatMessageAck(messageID uint32, accepted bool)
}
// SendMessage sends a given message using this channel, and returns the
// messageID, which will be used in ChatMessageAck when the peer acknowledges
// this message.
func (cc *ChatChannel) SendMessage(message string) uint32 {
return cc.SendMessageWithTime(message, time.Now())
}
// SendMessageWithTime is identical to SendMessage, but also sends the provided time.Time
// as a rough timestamp for when this message was originally sent. This should be used
// when retrying or sending queued messages.
func (cc *ChatChannel) SendMessageWithTime(message string, when time.Time) uint32 {
delta := time.Now().Sub(when) / time.Second
messageBuilder := new(utils.MessageBuilder)
messageID := cc.lastMessageID
cc.lastMessageID++
data := messageBuilder.ChatMessage(message, messageID, int64(delta))
cc.channel.SendMessage(data)
return messageID
}
// SendMessageOnChatChannel is a wrapper function which performs some necessary boilerplate
// to make sending messages easier.
func SendMessageOnChatChannel(channel *Channel, message string) (uint32, error) {
if channel != nil {
peerchannel, ok := channel.Handler.(*ChatChannel)
if ok {
return peerchannel.SendMessage(message), nil
}
return 0, errors.New("channel is not an im.ricochet.chat channel")
}
return 0, errors.New("channel pointer is nil")
}
// Acknowledge indicates that the given messageID was received, and whether
// it was accepted.
func (cc *ChatChannel) Acknowledge(messageID uint32, accepted bool) {
messageBuilder := new(utils.MessageBuilder)
cc.channel.SendMessage(messageBuilder.AckChatMessage(messageID, accepted))
}
// Type returns the type string for this channel, e.g. "im.ricochet.chat".
func (cc *ChatChannel) Type() string {
return "im.ricochet.chat"
}
// Closed is called when the channel is closed for any reason.
func (cc *ChatChannel) Closed(err error) {
}
// OnlyClientCanOpen - for chat channels any side can open
func (cc *ChatChannel) OnlyClientCanOpen() bool {
return false
}
// Singleton - for chat channels there can only be one instance per direction
func (cc *ChatChannel) Singleton() bool {
return true
}
// Bidirectional - for chat channels are not bidrectional
func (cc *ChatChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - chat channels require hidden service auth
func (cc *ChatChannel) RequiresAuthentication() string {
return "im.ricochet.auth.3dh"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cc *ChatChannel) OpenInbound(channel *Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cc.channel = channel
id := utils.GetRandNumber()
cc.lastMessageID = uint32(id.Uint64())
cc.channel.Pending = false
messageBuilder := new(utils.MessageBuilder)
go cc.Handler.OpenInbound()
return messageBuilder.AckOpenChannel(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cc *ChatChannel) OpenOutbound(channel *Channel) ([]byte, error) {
cc.channel = channel
id := utils.GetRandNumber()
cc.lastMessageID = uint32(id.Uint64())
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cc.Type()), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cc *ChatChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cc.channel.Pending = false
}
}
}
// Packet is called for each raw packet received on this channel.
func (cc *ChatChannel) Packet(data []byte) {
if !cc.channel.Pending {
res := new(Protocol_Data_Chat.Packet)
err := proto.Unmarshal(data, res)
if err == nil {
if res.GetChatMessage() != nil {
ack := cc.Handler.ChatMessage(res.GetChatMessage().GetMessageId(), time.Now(), res.GetChatMessage().GetMessageText())
cc.Acknowledge(res.GetChatMessage().GetMessageId(), ack)
} else if ack := res.GetChatAcknowledge(); ack != nil {
cc.Handler.ChatMessageAck(ack.GetMessageId(), ack.GetAccepted())
}
}
// We ignore invalid packets.
return
}
// Close the channel if it is being misused
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,131 @@
package channels
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/chat"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
"time"
)
func TestChatChannelOptions(t *testing.T) {
chatChannel := new(ChatChannel)
if chatChannel.Type() != "im.ricochet.chat" {
t.Errorf("ChatChannel has wrong type %s", chatChannel.Type())
}
if chatChannel.OnlyClientCanOpen() {
t.Errorf("ChatChannel should be able to be opened by everyone")
}
if !chatChannel.Singleton() {
t.Errorf("ChatChannel should be a Singelton")
}
if chatChannel.Bidirectional() {
t.Errorf("ChatChannel should not be bidirectional")
}
if chatChannel.RequiresAuthentication() != "im.ricochet.auth.3dh" {
t.Errorf("ChatChannel should require im.ricochet.auth.3dh. Instead requires: %s", chatChannel.RequiresAuthentication())
}
}
func TestChatChannelOpenOutbound(t *testing.T) {
chatChannel := new(ChatChannel)
channel := Channel{ID: 1}
response, err := chatChannel.OpenOutbound(&channel)
if err == nil {
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(response[:], res)
if res.GetOpenChannel() != nil {
// XXX
} else {
t.Errorf("ChatChannel OpenOutbound was not an OpenChannelRequest %v", err)
}
} else {
t.Errorf("Error while parsing openputput output: %v", err)
}
}
type TestChatChannelHandler struct {
}
func (tcch *TestChatChannelHandler) OpenInbound() {
}
func (tcch *TestChatChannelHandler) ChatMessage(messageID uint32, when time.Time, message string) bool {
return true
}
func (tcch *TestChatChannelHandler) ChatMessageAck(messageID uint32, accepted bool) {
}
func TestChatChannelOpenInbound(t *testing.T) {
messageBuilder := new(utils.MessageBuilder)
ocm := messageBuilder.OpenChannel(2, "im.ricochet.chat")
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ocm[:], res)
opm := res.GetOpenChannel()
chatChannel := new(ChatChannel)
chatChannel.Handler = new(TestChatChannelHandler)
channel := Channel{ID: 1}
response, err := chatChannel.OpenInbound(&channel, opm)
if err == nil {
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(response[:], res)
} else {
t.Errorf("Error while parsing chatchannel openinbound output: %v", err)
}
}
func TestChatChannelOperations(t *testing.T) {
// We test OpenOutboundElsewhere
chatChannel := new(ChatChannel)
chatChannel.Handler = new(TestChatChannelHandler)
channel := Channel{ID: 5}
channel.SendMessage = func(data []byte) {
res := new(Protocol_Data_Chat.Packet)
err := proto.Unmarshal(data, res)
if res.GetChatMessage() != nil {
if err == nil {
if res.GetChatMessage().GetMessageId() != 0 {
t.Log("Got Message ID:", res.GetChatMessage().GetMessageId())
return
}
t.Errorf("message id was 0 should be random")
return
}
t.Errorf("error sending chat message: %v", err)
}
}
chatChannel.OpenOutbound(&channel)
messageBuilder := new(utils.MessageBuilder)
ack := messageBuilder.AckOpenChannel(5)
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], res)
cr := res.GetChannelResult()
chatChannel.OpenOutboundResult(nil, cr)
if channel.Pending {
t.Errorf("After Successful Result ChatChannel Is Still Pending")
}
chat := messageBuilder.ChatMessage("message text", 0, 0)
chatChannel.Packet(chat)
chatChannel.SendMessage("hello")
ackMessage := messageBuilder.AckChatMessage(0, true)
chatChannel.Packet(ackMessage)
}

View File

@ -0,0 +1,162 @@
package channels
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/contact"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// Defining Versions
const (
InvalidContactNameError = utils.Error("InvalidContactNameError")
InvalidContactMessageError = utils.Error("InvalidContactMessageError")
InvalidContactRequestError = utils.Error("InvalidContactRequestError")
)
// ContactRequestChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.contact.request". The channel may be inbound or outbound.
type ContactRequestChannel struct {
// Methods of Handler are called for chat events on this channel
Handler ContactRequestChannelHandler
channel *Channel
// Properties of the request
Name string
Message string
}
// ContactRequestChannelHandler is implemented by an application type to receive
// events from a ContactRequestChannel.
//
// Note that ContactRequestChannelHandler is composable with other interfaces, including
// ConnectionHandler; there is no need to use a distinct type as a
// ContactRequestChannelHandler.
type ContactRequestChannelHandler interface {
ContactRequest(name string, message string) string
ContactRequestRejected()
ContactRequestAccepted()
ContactRequestError()
}
// OnlyClientCanOpen - only clients can open contact requests
func (crc *ContactRequestChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - only one contact request can be opened per side
func (crc *ContactRequestChannel) Singleton() bool {
return true
}
// Bidirectional - only clients can send messages
func (crc *ContactRequestChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - contact requests require hidden service auth
func (crc *ContactRequestChannel) RequiresAuthentication() string {
return "im.ricochet.auth.hidden-service"
}
// Type returns the type string for this channel, e.g. "im.ricochet.chat".
func (crc *ContactRequestChannel) Type() string {
return "im.ricochet.contact.request"
}
// Closed is called when the channel is closed for any reason.
func (crc *ContactRequestChannel) Closed(err error) {
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (crc *ContactRequestChannel) OpenInbound(channel *Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) {
crc.channel = channel
contactRequestI, err := proto.GetExtension(oc, Protocol_Data_ContactRequest.E_ContactRequest)
if err == nil {
contactRequest, check := contactRequestI.(*Protocol_Data_ContactRequest.ContactRequest)
if check {
if len(contactRequest.GetNickname()) > int(Protocol_Data_ContactRequest.Limits_NicknameMaxCharacters) {
// Violation of the Protocol
return nil, InvalidContactNameError
}
if len(contactRequest.GetMessageText()) > int(Protocol_Data_ContactRequest.Limits_MessageMaxCharacters) {
// Violation of the Protocol
return nil, InvalidContactMessageError
}
crc.Name = contactRequest.GetNickname()
crc.Message = contactRequest.GetMessageText()
result := crc.Handler.ContactRequest(contactRequest.GetNickname(), contactRequest.GetMessageText())
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.ReplyToContactRequestOnResponse(channel.ID, result), nil
}
}
return nil, InvalidContactRequestError
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (crc *ContactRequestChannel) OpenOutbound(channel *Channel) ([]byte, error) {
crc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenContactRequestChannel(channel.ID, crc.Name, crc.Message), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (crc *ContactRequestChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
responseI, err := proto.GetExtension(crm, Protocol_Data_ContactRequest.E_Response)
if err == nil {
response, check := responseI.(*Protocol_Data_ContactRequest.Response)
if check {
crc.handleStatus(response.GetStatus().String())
return
}
}
}
}
}
// SendResponse sends a contact request status response to the requester.
func (crc *ContactRequestChannel) SendResponse(status string) {
messageBuilder := new(utils.MessageBuilder)
crc.channel.SendMessage(messageBuilder.ReplyToContactRequest(crc.channel.ID, status))
crc.channel.CloseChannel()
}
func (crc *ContactRequestChannel) handleStatus(status string) {
switch status {
case "Accepted":
crc.Handler.ContactRequestAccepted()
crc.channel.CloseChannel()
case "Pending":
break
case "Rejected":
crc.Handler.ContactRequestRejected()
crc.channel.CloseChannel()
case "Error":
crc.Handler.ContactRequestError()
crc.channel.CloseChannel()
}
}
// Packet is called for each raw packet received on this channel.
func (crc *ContactRequestChannel) Packet(data []byte) {
if !crc.channel.Pending {
response := new(Protocol_Data_ContactRequest.Response)
err := proto.Unmarshal(data, response)
if err == nil {
crc.handleStatus(response.GetStatus().String())
}
}
}

View File

@ -0,0 +1,282 @@
package channels
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/contact"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
)
type TestContactRequestHandler struct {
Received bool
}
func (tcrh *TestContactRequestHandler) ContactRequest(name string, message string) string {
if name == "test_nickname" && message == "test_message" {
tcrh.Received = true
}
return "Pending"
}
func (tcrh *TestContactRequestHandler) ContactRequestRejected() {
}
func (tcrh *TestContactRequestHandler) ContactRequestAccepted() {
}
func (tcrh *TestContactRequestHandler) ContactRequestError() {
}
func TestContactRequestOptions(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
if contactRequestChannel.Type() != "im.ricochet.contact.request" {
t.Errorf("ContactRequestChannel has wrong type %s", contactRequestChannel.Type())
}
if !contactRequestChannel.OnlyClientCanOpen() {
t.Errorf("ContactRequestChannel Should be Client Open Only")
}
if !contactRequestChannel.Singleton() {
t.Errorf("ContactRequestChannel Should be a Singelton")
}
if contactRequestChannel.Bidirectional() {
t.Errorf("ContactRequestChannel Should not be bidirectional")
}
if contactRequestChannel.RequiresAuthentication() != "im.ricochet.auth.hidden-service" {
t.Errorf("ContactRequestChannel should requires im.ricochet.auth.hidden-service Authentication. Instead defines: %s", contactRequestChannel.RequiresAuthentication())
}
}
func TestContactRequestOpenOutbound(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
channel.CloseChannel = func() {}
response, err := contactRequestChannel.OpenOutbound(&channel)
if err == nil {
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(response[:], res)
if res.GetOpenChannel() != nil {
// XXX
} else {
t.Errorf("ContactReuqest OpenOutbound was not an OpenChannelRequest %v", err)
}
} else {
t.Errorf("Error while parsing openputput output: %v", err)
}
}
func TestContactRequestOpenOutboundResult(t *testing.T) {
contactRequestChannel := &ContactRequestChannel{
Name: "test_nickname",
Message: "test_message",
Handler: &TestContactRequestHandler{},
}
channel := Channel{ID: 1}
channel.CloseChannel = func() {}
contactRequestChannel.OpenOutbound(&channel)
messageBuilder := new(utils.MessageBuilder)
ack := messageBuilder.ReplyToContactRequestOnResponse(1, "Accepted")
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], res)
cr := res.GetChannelResult()
contactRequestChannel.OpenOutboundResult(nil, cr)
}
func TestContactRequestOpenInbound(t *testing.T) {
opm := BuildOpenChannel("test_nickname", "test_message")
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
channel.CloseChannel = func() {}
response, err := contactRequestChannel.OpenInbound(&channel, opm)
if err == nil {
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(response[:], res)
responseI, err := proto.GetExtension(res.GetChannelResult(), Protocol_Data_ContactRequest.E_Response)
if err == nil {
response, check := responseI.(*Protocol_Data_ContactRequest.Response)
if check {
if response.GetStatus().String() != "Pending" {
t.Errorf("Contact Request Response should have been Pending, but instead was: %v", response.GetStatus().String())
}
} else {
t.Errorf("Error while parsing openinbound output: %v", err)
}
} else {
t.Errorf("Error while parsing openinbound output: %v", err)
}
} else {
t.Errorf("Error while parsing openinbound output: %v", err)
}
if !handler.Received {
t.Errorf("Contact Request was not received by Handler")
}
}
func TestContactRequestPacket(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
contactRequestChannel.OpenOutbound(&channel)
messageBuilder := new(utils.MessageBuilder)
ack := messageBuilder.ReplyToContactRequestOnResponse(1, "Pending")
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], res)
cr := res.GetChannelResult()
contactRequestChannel.OpenOutboundResult(nil, cr)
ackp := messageBuilder.ReplyToContactRequest(1, "Accepted")
closed := false
channel.CloseChannel = func() { closed = true }
contactRequestChannel.Packet(ackp)
if closed == false {
t.Errorf("Channel Should Have Been Closed")
}
}
func TestContactRequestPending(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
contactRequestChannel.OpenOutbound(&channel)
messageBuilder := new(utils.MessageBuilder)
ack := messageBuilder.ReplyToContactRequestOnResponse(1, "Pending")
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], res)
cr := res.GetChannelResult()
contactRequestChannel.OpenOutboundResult(nil, cr)
ackp := messageBuilder.ReplyToContactRequest(1, "Pending")
closed := false
channel.CloseChannel = func() { closed = true }
contactRequestChannel.Packet(ackp)
if closed {
t.Errorf("Channel Should Not Have Been Closed")
}
}
func TestContactRequestSend(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
channel := Channel{ID: 1}
channel.SendMessage = func(message []byte) {}
closed := false
channel.CloseChannel = func() { closed = true }
contactRequestChannel.OpenOutbound(&channel)
contactRequestChannel.SendResponse("Accepted")
if closed != true {
t.Errorf("Channel Should Not Have Been Closed")
}
}
func TestContactRequestRejected(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
contactRequestChannel.OpenOutbound(&channel)
messageBuilder := new(utils.MessageBuilder)
ack := messageBuilder.ReplyToContactRequestOnResponse(1, "Pending")
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], res)
cr := res.GetChannelResult()
contactRequestChannel.OpenOutboundResult(nil, cr)
ackp := messageBuilder.ReplyToContactRequest(1, "Rejected")
closed := false
channel.CloseChannel = func() { closed = true }
contactRequestChannel.Packet(ackp)
if closed == false {
t.Errorf("Channel Should Have Been Closed")
}
}
func TestContactRequestError(t *testing.T) {
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
contactRequestChannel.OpenOutbound(&channel)
messageBuilder := new(utils.MessageBuilder)
ack := messageBuilder.ReplyToContactRequestOnResponse(1, "Pending")
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], res)
cr := res.GetChannelResult()
contactRequestChannel.OpenOutboundResult(nil, cr)
ackp := messageBuilder.ReplyToContactRequest(1, "Error")
closed := false
channel.CloseChannel = func() { closed = true }
contactRequestChannel.Packet(ackp)
if closed == false {
t.Errorf("Channel Should Have Been Closed")
}
}
func BuildOpenChannel(nickname string, message string) *Protocol_Data_Control.OpenChannel {
// Construct the Open Authentication Channel Message
messageBuilder := new(utils.MessageBuilder)
ocm := messageBuilder.OpenContactRequestChannel(1, nickname, message)
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ocm[:], res)
return res.GetOpenChannel()
}
func TestInvalidNickname(t *testing.T) {
opm := BuildOpenChannel("this nickname is far too long at well over the limit of 30 characters", "test_message")
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
_, err := contactRequestChannel.OpenInbound(&channel, opm)
if err == nil {
t.Errorf("Open Inbound should have failed because of invalid nickname")
}
}
func TestInvalidMessage(t *testing.T) {
var message string
for i := 0; i < 2001; i++ {
message += "a"
}
opm := BuildOpenChannel("test_nickname", message)
contactRequestChannel := new(ContactRequestChannel)
handler := new(TestContactRequestHandler)
contactRequestChannel.Handler = handler
channel := Channel{ID: 1}
_, err := contactRequestChannel.OpenInbound(&channel, opm)
if err == nil {
t.Errorf("Open Inbound should have failed because of invalid message")
}
}

51
channels/handler.go Normal file
View File

@ -0,0 +1,51 @@
package channels
import (
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
)
// Handler reacts to low-level events on a protocol channel. There
// should be a unique instance of a ChannelHandler type per channel.
//
// Applications generally don't need to implement ChannelHandler directly;
// instead, use the built-in implementations for common channel types, and
// their individual callback interfaces. ChannelHandler is useful when
// implementing new channel types, or modifying low level default behavior.
type Handler interface {
// Type returns the type string for this channel, e.g. "im.ricochet.chat".
Type() string
// Closed is called when the channel is closed for any reason.
Closed(err error)
// OnlyClientCanOpen indicates if only a client can open a given channel
OnlyClientCanOpen() bool
// Singleton indicates if a channel can only have one instance per direction
Singleton() bool
// Bidirectional indicates if messages can be send by either side
Bidirectional() bool
// RequiresAuthentication describes what authentication is needed for the channel
RequiresAuthentication() string
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
OpenInbound(channel *Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error)
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
OpenOutbound(channel *Channel) ([]byte, error)
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
OpenOutboundResult(err error, raw *Protocol_Data_Control.ChannelResult)
// Packet is called for each raw packet received on this channel.
Packet(data []byte)
}

View File

@ -0,0 +1,174 @@
package inbound
import (
"crypto/rand"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
)
// Server3DHAuthChannel wraps implementation of im.ricochet.auth.hidden-service"
type Server3DHAuthChannel struct {
// PrivateKey must be set for client-side authentication channels
ServerIdentity identity.Identity
// Callbacks
ServerAuthValid func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool)
ServerAuthInvalid func(err error)
// Internal state
clientPubKey, clientEphmeralPublicKey, serverEphemeralPublicKey ed25519.PublicKey
serverEphemeralPrivateKey ed25519.PrivateKey
channel *channels.Channel
}
// Type returns the type string for this channel, e.g. "im.ricochet.chat".
func (ah *Server3DHAuthChannel) Type() string {
return "im.ricochet.auth.3dh"
}
// Singleton Returns whether or not the given channel type is a singleton
func (ah *Server3DHAuthChannel) Singleton() bool {
return true
}
// OnlyClientCanOpen ...
func (ah *Server3DHAuthChannel) OnlyClientCanOpen() bool {
return true
}
// Bidirectional Returns whether or not the given channel allows anyone to send messages
func (ah *Server3DHAuthChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication Returns whether or not the given channel type requires authentication
func (ah *Server3DHAuthChannel) RequiresAuthentication() string {
return "none"
}
// Closed is called when the channel is closed for any reason.
func (ah *Server3DHAuthChannel) Closed(err error) {
}
// OpenInbound is the first method called for an inbound channel request.
// Infof an error is returned, the channel is rejected. Infof a RawMessage is
// returned, it will be sent as the ChannelResult message.
// Remote -> [Open Authentication Channel] -> Local
func (ah *Server3DHAuthChannel) OpenInbound(channel *channels.Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) {
ah.channel = channel
clientPublicKey, _ := proto.GetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientPublicKey)
clientEphmeralPublicKey, _ := proto.GetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientEphmeralPublicKey)
clientPubKeyBytes := clientPublicKey.([]byte)
ah.clientPubKey = ed25519.PublicKey(clientPubKeyBytes[:])
clientEphmeralPublicKeyBytes := clientEphmeralPublicKey.([]byte)
ah.clientEphmeralPublicKey = ed25519.PublicKey(clientEphmeralPublicKeyBytes[:])
clientHostname := utils.GetTorV3Hostname(clientPubKeyBytes)
log.Debugf("Received inbound auth 3DH request from %v", clientHostname)
// Generate Ephemeral Keys
pubkey, privkey, _ := ed25519.GenerateKey(rand.Reader)
ah.serverEphemeralPublicKey = pubkey
ah.serverEphemeralPrivateKey = privkey
var serverPubKeyBytes, serverEphemeralPubKeyBytes [32]byte
copy(serverPubKeyBytes[:], ah.ServerIdentity.PublicKeyBytes()[:])
copy(serverEphemeralPubKeyBytes[:], ah.serverEphemeralPublicKey[:])
messageBuilder := new(utils.MessageBuilder)
channel.Pending = false
return messageBuilder.Confirm3EDHAuthChannel(ah.channel.ID, serverPubKeyBytes, serverEphemeralPubKeyBytes), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// Infof an error is returned, the channel is not opened. Infof a RawMessage is
// returned, it will be sent as the OpenChannel message.
// Local -> [Open Authentication Channel] -> Remote
func (ah *Server3DHAuthChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
return nil, errors.New("server is not allowed to open 3dh channels")
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. Infof `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
// Input: Remote -> [ChannelResult] -> {Client}
// Output: {Client} -> [Proof] -> Remote
func (ah *Server3DHAuthChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
//
}
// Packet is called for each raw packet received on this channel.
// Input: Client -> [Proof] -> Remote
// OR
// Input: Remote -> [Result] -> Client
func (ah *Server3DHAuthChannel) Packet(data []byte) {
res := new(Protocol_Data_Auth_TripleEDH.Packet)
err := proto.Unmarshal(data[:], res)
if err != nil {
ah.channel.CloseChannel()
return
}
if res.GetProof() != nil && ah.channel.Direction == channels.Inbound {
// Server Identity <-> Client Ephemeral
secret1 := ah.ServerIdentity.EDH(ah.clientEphmeralPublicKey)
// Server Ephemeral <-> Client Identity
secret2 := utils.EDH(ah.serverEphemeralPrivateKey, ah.clientPubKey)
// Ephemeral <-> Ephemeral
secret3 := utils.EDH(ah.serverEphemeralPrivateKey, ah.clientEphmeralPublicKey)
var secret [96]byte
copy(secret[0:32], secret1[:])
copy(secret[32:64], secret2[:])
copy(secret[64:96], secret3[:])
pkey := pbkdf2.Key(secret[:], secret[:], 4096, 32, sha3.New512)
var key [32]byte
copy(key[:], pkey[:])
var decryptNonce [24]byte
ciphertext := res.GetProof().GetProof()
if len(ciphertext) > 24 {
copy(decryptNonce[:], ciphertext[:24])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key)
if ok && string(decrypted) == "Hello World" {
allowed, known := ah.ServerAuthValid(utils.GetTorV3Hostname(ah.clientPubKey), ah.clientPubKey)
ah.channel.DelegateAuthorization()
ah.channel.DelegateEncryption(key)
log.Debugf("3DH Session Decrypted OK. Authenticating Connection!")
messageBuilder := new(utils.MessageBuilder)
result := messageBuilder.AuthResult3DH(allowed, known)
ah.channel.SendMessage(result)
ah.channel.CloseChannel()
return
}
}
messageBuilder := new(utils.MessageBuilder)
result := messageBuilder.AuthResult3DH(false, false)
ah.channel.SendMessage(result)
}
// Any other combination of packets is completely invalid
// Fail the Authorization right here.
ah.channel.CloseChannel()
}

View File

@ -0,0 +1,126 @@
package inbound
import (
"crypto/rand"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/outbound"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/ed25519"
"testing"
)
func TestServer3DHAuthChannel(t *testing.T) {
cc := new(channels.Channel)
cc.ID = 1
closed := false
cc.CloseChannel = func() { closed = true }
cc.DelegateEncryption = func([32]byte) {}
clientChannel := new(outbound.Client3DHAuthChannel)
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
cid := identity.InitializeV3("", &priv, &pub)
clientChannel.ClientIdentity = cid
ocb, _ := clientChannel.OpenOutbound(cc)
packet := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ocb, packet)
s3dhchannel := new(Server3DHAuthChannel)
pub, priv, _ = ed25519.GenerateKey(rand.Reader)
sid := identity.InitializeV3("", &priv, &pub)
s3dhchannel.ServerIdentity = sid
clientChannel.ServerHostname = utils.GetTorV3Hostname(pub)
cr, _ := s3dhchannel.OpenInbound(cc, packet.GetOpenChannel())
proto.Unmarshal(cr, packet)
if packet.GetChannelResult() != nil {
authPacket := new(Protocol_Data_Auth_TripleEDH.Packet)
var lastMessage []byte
cc.SendMessage = func(message []byte) {
t.Logf("Received: %x", message)
proto.Unmarshal(message, authPacket)
lastMessage = message
}
clientChannel.OpenOutboundResult(nil, packet.GetChannelResult())
if closed == true {
t.Fatalf("Should not have closed channel!")
}
if authPacket.Proof == nil {
t.Errorf("Was expected a Proof Packet, instead %v", authPacket)
}
s3dhchannel.ServerAuthValid = func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) {
if hostname != clientChannel.ClientIdentity.Hostname() {
t.Errorf("Hostname and public key did not match %v %v", hostname, pub)
}
return true, true
}
cc.DelegateAuthorization = func() {}
s3dhchannel.Packet(lastMessage)
} else {
t.Errorf("Should have received a Channel Response from OpenInbound: %v", packet)
}
}
func TestServer3DHAuthChannelReject(t *testing.T) {
cc := new(channels.Channel)
cc.ID = 1
cc.CloseChannel = func() {}
cc.DelegateEncryption = func([32]byte) {}
clientChannel := new(outbound.Client3DHAuthChannel)
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
cid := identity.InitializeV3("", &priv, &pub)
clientChannel.ClientIdentity = cid
ocb, _ := clientChannel.OpenOutbound(cc)
packet := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ocb, packet)
s3dhchannel := new(Server3DHAuthChannel)
pub, priv, _ = ed25519.GenerateKey(rand.Reader)
sid := identity.InitializeV3("", &priv, &pub)
s3dhchannel.ServerIdentity = sid
clientChannel.ServerHostname = utils.GetTorV3Hostname(pub)
cr, _ := s3dhchannel.OpenInbound(cc, packet.GetOpenChannel())
proto.Unmarshal(cr, packet)
if packet.GetChannelResult() != nil {
authPacket := new(Protocol_Data_Auth_TripleEDH.Packet)
var lastMessage []byte
cc.SendMessage = func(message []byte) {
proto.Unmarshal(message, authPacket)
// Replace the Auth Proof Packet to cause this to fail.
if authPacket.GetProof() != nil {
authPacket.GetProof().Proof = []byte{}
lastMessage, _ = proto.Marshal(authPacket)
}
}
clientChannel.OpenOutboundResult(nil, packet.GetChannelResult())
if authPacket.Proof == nil {
t.Errorf("Was expected a Proof Packet, instead %v", authPacket)
}
s3dhchannel.ServerAuthInvalid = func(err error) {
if err == nil {
t.Fatal("Server Auth Should have been invalid!")
}
}
cc.DelegateAuthorization = func() {}
s3dhchannel.Packet(lastMessage)
} else {
t.Errorf("Should have received a Channel Response from OpenInbound: %v", packet)
}
}

View File

@ -0,0 +1,173 @@
package outbound
import (
"crypto/rand"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
)
// Client3DHAuthChannel wraps implementation of im.ricochet.auth.hidden-service"
type Client3DHAuthChannel struct {
// PrivateKey must be set for client-side authentication channels
ClientIdentity identity.Identity
ServerHostname string
ClientAuthResult func(bool, bool)
// Internal state
serverPubKey, serverEphemeralPublicKey, clientEphemeralPublicKey ed25519.PublicKey
clientEphemeralPrivateKey ed25519.PrivateKey
channel *channels.Channel
key [32]byte
}
// Type returns the type string for this channel, e.g. "im.ricochet.chat".
func (ah *Client3DHAuthChannel) Type() string {
return "im.ricochet.auth.3dh"
}
// Singleton Returns whether or not the given channel type is a singleton
func (ah *Client3DHAuthChannel) Singleton() bool {
return true
}
// OnlyClientCanOpen ...
func (ah *Client3DHAuthChannel) OnlyClientCanOpen() bool {
return true
}
// Bidirectional Returns whether or not the given channel allows anyone to send messages
func (ah *Client3DHAuthChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication Returns whether or not the given channel type requires authentication
func (ah *Client3DHAuthChannel) RequiresAuthentication() string {
return "none"
}
// Closed is called when the channel is closed for any reason.
func (ah *Client3DHAuthChannel) Closed(err error) {
}
// OpenInbound is the first method called for an inbound channel request.
// Infof an error is returned, the channel is rejected. Infof a RawMessage is
// returned, it will be sent as the ChannelResult message.
// Remote -> [Open Authentication Channel] -> Local
func (ah *Client3DHAuthChannel) OpenInbound(channel *channels.Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) {
return nil, errors.New("server is not allowed to open inbound auth.3dh channels")
}
// OpenOutbound is the first method called for an outbound channel request.
// Infof an error is returned, the channel is not opened. Infof a RawMessage is
// returned, it will be sent as the OpenChannel message.
// Local -> [Open Authentication Channel] -> Remote
func (ah *Client3DHAuthChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
ah.channel = channel
log.Debugf("Opening an outbound connection to %v", ah.ServerHostname)
// Generate Ephemeral Keys
pubkey, privkey, _ := ed25519.GenerateKey(rand.Reader)
ah.clientEphemeralPublicKey = pubkey
ah.clientEphemeralPrivateKey = privkey
messageBuilder := new(utils.MessageBuilder)
channel.Pending = false
var clientPubKeyBytes, clientEphemeralPubKeyBytes [32]byte
copy(clientPubKeyBytes[:], ah.ClientIdentity.PublicKeyBytes()[:])
copy(clientEphemeralPubKeyBytes[:], ah.clientEphemeralPublicKey[:])
return messageBuilder.Open3EDHAuthenticationChannel(ah.channel.ID, clientPubKeyBytes, clientEphemeralPubKeyBytes), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. Infof `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
// Input: Remote -> [ChannelResult] -> {Client}
// Output: {Client} -> [Proof] -> Remote
func (ah *Client3DHAuthChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
serverPublicKey, _ := proto.GetExtension(crm, Protocol_Data_Auth_TripleEDH.E_ServerPublicKey)
serverEphemeralPublicKey, _ := proto.GetExtension(crm, Protocol_Data_Auth_TripleEDH.E_ServerEphmeralPublicKey)
serverPubKeyBytes := serverPublicKey.([]byte)
ah.serverPubKey = ed25519.PublicKey(serverPubKeyBytes[:])
if utils.GetTorV3Hostname(ah.serverPubKey) != ah.ServerHostname {
ah.channel.CloseChannel()
return
}
serverEphmeralPublicKeyBytes := serverEphemeralPublicKey.([]byte)
ah.serverEphemeralPublicKey = ed25519.PublicKey(serverEphmeralPublicKeyBytes[:])
log.Debugf("Public Keys Exchanged. Deriving Encryption Keys and Sending Encrypted Test Message")
// Server Ephemeral <-> Client Identity
secret1 := utils.EDH(ah.clientEphemeralPrivateKey, ah.serverPubKey)
// Server Identity <-> Client Ephemeral
secret2 := ah.ClientIdentity.EDH(ah.serverEphemeralPublicKey)
// Ephemeral <-> Ephemeral
secret3 := utils.EDH(ah.clientEphemeralPrivateKey, ah.serverEphemeralPublicKey)
var secret [96]byte
copy(secret[0:32], secret1[:])
copy(secret[32:64], secret2[:])
copy(secret[64:96], secret3[:])
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
pkey := pbkdf2.Key(secret[:], secret[:], 4096, 32, sha3.New512)
copy(ah.key[:], pkey[:])
encrypted := secretbox.Seal(nonce[:], []byte("Hello World"), &nonce, &ah.key)
messageBuilder := new(utils.MessageBuilder)
proof := messageBuilder.Proof3DH(encrypted)
ah.channel.SendMessage(proof)
ah.channel.DelegateEncryption(ah.key)
}
// Packet is called for each raw packet received on this channel.
// Input: Client -> [Proof] -> Remote
// OR
// Input: Remote -> [Result] -> Client
func (ah *Client3DHAuthChannel) Packet(data []byte) {
res := new(Protocol_Data_Auth_TripleEDH.Packet)
err := proto.Unmarshal(data[:], res)
if err != nil {
ah.channel.CloseChannel()
return
}
if res.GetResult() != nil {
ah.ClientAuthResult(res.GetResult().GetAccepted(), res.GetResult().GetIsKnownContact())
if res.GetResult().GetAccepted() {
log.Debugf("3DH Session Accepted OK. Authenticated! Connection!")
ah.channel.DelegateAuthorization()
}
return
}
// Any other combination of packets is completely invalid
// Fail the Authorization right here.
ah.channel.CloseChannel()
}

View File

@ -0,0 +1,29 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
)
// AuthorizationManager helps keep track of permissions for a connection
type AuthorizationManager struct {
Authorizations map[string]bool
}
// Init sets up an AuthorizationManager to be used.
func (am *AuthorizationManager) Init() {
am.Authorizations = make(map[string]bool)
}
// AddAuthorization adds the string authz to the map of allowed authorizations
func (am *AuthorizationManager) AddAuthorization(authz string) {
am.Authorizations[authz] = true
}
// Authorized returns no error in the case an authz type is authorized, error otherwise.
func (am *AuthorizationManager) Authorized(authz string) error {
_, authed := am.Authorizations[authz]
if !authed {
return utils.UnauthorizedActionError
}
return nil
}

View File

@ -0,0 +1,18 @@
package connection
import (
"testing"
)
func TestAuthorizationManager(t *testing.T) {
am := new(AuthorizationManager)
am.Init()
am.AddAuthorization("test")
if am.Authorized("test") != nil {
t.Errorf("Authorized(test) should return nil, instead returned error: %v", am.Authorized("test"))
}
if am.Authorized("not_authed") == nil {
t.Errorf("Authorized(not_authed) should return error, instead returned nil: %v", am.Authorized("not_authed"))
}
}

View File

@ -0,0 +1,62 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
)
// AutoConnectionHandler implements the ConnectionHandler interface on behalf of
// the provided application type by automatically providing support for any
// built-in channel type whose high level interface is implemented by the
// application. For example, if the application's type implements the
// ChatChannelHandler interface, `im.ricochet.chat` will be available to the peer.
//
// The application handler can be any other type. To override or augment any of
// AutoConnectionHandler's behavior (such as adding new channel types, or reacting
// to connection close events), this type can be embedded in the type that it serves.
type AutoConnectionHandler struct {
handlerMap map[string]func() channels.Handler
connection *Connection
}
// Init ...
// TODO: Split this into client and server init
func (ach *AutoConnectionHandler) Init() {
ach.handlerMap = make(map[string]func() channels.Handler)
}
// OnReady ...
func (ach *AutoConnectionHandler) OnReady(oc *Connection) {
ach.connection = oc
}
// OnClosed is called when the OpenConnection has closed for any reason.
func (ach *AutoConnectionHandler) OnClosed(err error) {
}
// GetSupportedChannelTypes returns a list of channel types that are registered with the handler.
func (ach *AutoConnectionHandler) GetSupportedChannelTypes() []string {
supported := []string{}
for k := range ach.handlerMap {
supported = append(supported, k)
}
return supported
}
// RegisterChannelHandler ...
func (ach *AutoConnectionHandler) RegisterChannelHandler(ctype string, handler func() channels.Handler) {
_, exists := ach.handlerMap[ctype]
if !exists {
ach.handlerMap[ctype] = handler
}
}
// OnOpenChannelRequest ...
func (ach *AutoConnectionHandler) OnOpenChannelRequest(ctype string) (channels.Handler, error) {
handler, ok := ach.handlerMap[ctype]
if ok {
h := handler()
return h, nil
}
return nil, utils.UnknownChannelTypeError
}

View File

@ -0,0 +1,43 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/inbound"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
)
// Test sending valid packets
func TestInit(t *testing.T) {
ach := new(AutoConnectionHandler)
ach.Init()
ach.RegisterChannelHandler("im.ricochet.auth.3dh", func() channels.Handler {
return &inbound.Server3DHAuthChannel{}
})
// Construct the Open Authentication Channel Message
messageBuilder := new(utils.MessageBuilder)
ocm := messageBuilder.Open3EDHAuthenticationChannel(1, [32]byte{}, [32]byte{})
// We have just constructed this so there is little
// point in doing error checking here in the test
res := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ocm[:], res)
opm := res.GetOpenChannel()
handler, err := ach.OnOpenChannelRequest(opm.GetChannelType())
if err == nil {
if handler.Type() != "im.ricochet.auth.3dh" {
t.Errorf("Failed to authentication handler: %v", handler.Type())
}
} else {
t.Errorf("Failed to build handler: %v", err)
}
types := ach.GetSupportedChannelTypes()
if len(types) != 1 {
t.Errorf("Expected only im.ricochet.auth.hidden-service to be supported instead got: %v", types)
}
}

View File

@ -0,0 +1,129 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"sync"
)
// ChannelManager encapsulates the logic for server and client side assignment
// and removal of channels.
type ChannelManager struct {
channels map[int32]*channels.Channel
nextFreeChannel int32
isClient bool
lock sync.Mutex // ChannelManager may be accessed by multiple thread, so we need to protect the map.
}
// NewClientChannelManager construsts a new channel manager enforcing behaviour
// of a ricochet client
func NewClientChannelManager() *ChannelManager {
channelManager := new(ChannelManager)
channelManager.channels = make(map[int32]*channels.Channel)
channelManager.nextFreeChannel = 1
channelManager.isClient = true
return channelManager
}
// NewServerChannelManager construsts a new channel manager enforcing behaviour
// from a ricochet server
func NewServerChannelManager() *ChannelManager {
channelManager := new(ChannelManager)
channelManager.channels = make(map[int32]*channels.Channel)
channelManager.nextFreeChannel = 2
channelManager.isClient = false
return channelManager
}
// OpenChannelRequest constructs a channel type ready for processing given a request
// from the client.
func (cm *ChannelManager) OpenChannelRequest(chandler channels.Handler) (*channels.Channel, error) {
// Some channels only allow us to open one of them per connection
if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Outbound) != nil {
return nil, utils.AttemptToOpenMoreThanOneSingletonChannelError
}
channel := new(channels.Channel)
channel.ID = cm.nextFreeChannel
cm.nextFreeChannel += 2
channel.Type = chandler.Type()
channel.Handler = chandler
channel.Pending = true
channel.Direction = channels.Outbound
cm.lock.Lock()
cm.channels[channel.ID] = channel
cm.lock.Unlock()
return channel, nil
}
// OpenChannelRequestFromPeer constructs a channel type ready for processing given a request
// from the remote peer.
func (cm *ChannelManager) OpenChannelRequestFromPeer(channelID int32, chandler channels.Handler) (*channels.Channel, error) {
if cm.isClient && (channelID%2) != 0 {
// Server is trying to open odd numbered channels
return nil, utils.ServerAttemptedToOpenEvenNumberedChannelError
} else if !cm.isClient && (channelID%2) == 0 {
// Server is trying to open odd numbered channels
return nil, utils.ClientAttemptedToOpenOddNumberedChannelError
}
cm.lock.Lock()
_, exists := cm.channels[channelID]
if exists {
cm.lock.Unlock()
return nil, utils.ChannelIDIsAlreadyInUseError
}
cm.lock.Unlock()
// Some channels only allow us to open one of them per connection
if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Inbound) != nil {
return nil, utils.AttemptToOpenMoreThanOneSingletonChannelError
}
channel := new(channels.Channel)
channel.ID = channelID
channel.Type = chandler.Type()
channel.Handler = chandler
channel.Pending = true
channel.Direction = channels.Inbound
cm.lock.Lock()
cm.channels[channelID] = channel
cm.lock.Unlock()
return channel, nil
}
// Channel finds an open or pending `type` channel in the direction `way` (Inbound
// or Outbound), and returns the associated state. Returns nil if no matching channel
// exists or if multiple matching channels exist.
func (cm *ChannelManager) Channel(ctype string, way channels.Direction) *channels.Channel {
cm.lock.Lock()
var foundChannel *channels.Channel
for _, channel := range cm.channels {
if channel.Handler.Type() == ctype && channel.Direction == way {
if foundChannel == nil {
foundChannel = channel
} else {
// we have found multiple channels.
return nil
}
}
}
cm.lock.Unlock()
return foundChannel
}
// GetChannel finds and returns a given channel if it is found
func (cm *ChannelManager) GetChannel(channelID int32) (*channels.Channel, bool) {
cm.lock.Lock()
channel, found := cm.channels[channelID]
cm.lock.Unlock()
return channel, found
}
// RemoveChannel removes a given channel id.
func (cm *ChannelManager) RemoveChannel(channelID int32) {
cm.lock.Lock()
delete(cm.channels, channelID)
cm.lock.Unlock()
}

View File

@ -0,0 +1,89 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"testing"
)
type OverrideChatChannel struct {
channels.ChatChannel
}
// Singleton - for chat channels there can only be one instance per direction
func (cc *OverrideChatChannel) Singleton() bool {
return false
}
func TestClientManagerDuplicateMultiple(t *testing.T) {
ccm := NewClientChannelManager()
chatChannel := new(OverrideChatChannel)
_, err := ccm.OpenChannelRequestFromPeer(2, chatChannel)
if err != nil {
t.Errorf("Opening ChatChannel should have succeeded, instead: %v", err)
}
_, err = ccm.OpenChannelRequestFromPeer(4, chatChannel)
if err != nil {
t.Errorf("Opening ChatChannel should have succeeded, instead: %v", err)
}
channel := ccm.Channel("im.ricochet.chat", channels.Inbound)
if channel != nil {
t.Errorf("Get ChatChannel should have failed because there are 2 of them")
}
}
func TestClientManagerDuplicateChannel(t *testing.T) {
ccm := NewClientChannelManager()
chatChannel := new(channels.ChatChannel)
_, err := ccm.OpenChannelRequestFromPeer(2, chatChannel)
if err != nil {
t.Errorf("Opening ChatChannel should have succeeded, instead: %v", err)
}
_, err = ccm.OpenChannelRequestFromPeer(2, chatChannel)
if err == nil {
t.Errorf("Opening ChatChannel should have failed")
}
_, err = ccm.OpenChannelRequestFromPeer(4, chatChannel)
if err == nil {
t.Errorf("Opening ChatChannel should have failed because there should be only 1")
}
}
func TestClientManagerBadServer(t *testing.T) {
ccm := NewClientChannelManager()
// Servers are not allowed to open odd numbered channels
_, err := ccm.OpenChannelRequestFromPeer(3, nil)
if err == nil {
t.Errorf("OpenChannelRequestFromPeer should have failed")
}
}
func TestServerManagerBadClient(t *testing.T) {
scm := NewServerChannelManager()
// Clients are not allowed to open even numbered channels
_, err := scm.OpenChannelRequestFromPeer(2, nil)
if err == nil {
t.Errorf("OpenChannelRequestFromPeer should have failed")
}
}
func TestLocalDuplicate(t *testing.T) {
scm := NewServerChannelManager()
chatChannel := new(channels.ChatChannel)
channel, err := scm.OpenChannelRequest(chatChannel)
if err != nil {
t.Errorf("OpenChannelRequest should not have failed: %v", err)
}
_, err = scm.OpenChannelRequest(chatChannel)
if err == nil {
t.Errorf("OpenChannelRequest should have failed")
}
scm.RemoveChannel(channel.ID)
_, err = scm.OpenChannelRequest(chatChannel)
if err != nil {
t.Errorf("OpenChannelRequest should not have failed: %v", err)
}
}

477
connection/connection.go Normal file
View File

@ -0,0 +1,477 @@
package connection
import (
"context"
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"io"
"sync"
)
// Connection encapsulates the state required to maintain a connection to
// a ricochet service.
type Connection struct {
utils.RicochetNetwork
channelManager *ChannelManager
ctrlChannel ControlChannel
// Ricochet Network Loop
packetChannel chan utils.RicochetData
errorChannel chan error
breakChannel chan bool
breakResultChannel chan error
unlockChannel chan bool
unlockResponseChannel chan bool
messageBuilder utils.MessageBuilder
closed bool
closingLock sync.Mutex
closing bool
// This mutex is exclusively for preventing races during blocking
// interactions with Process; specifically Do and Break. Don't use
// it for anything else. See those functions for an explanation.
processBlockMutex sync.Mutex
conn io.ReadWriteCloser
IsInbound bool
am AuthorizationManager
RemoteHostname string
SupportChannels []string
}
func (rc *Connection) init() {
rc.packetChannel = make(chan utils.RicochetData)
rc.errorChannel = make(chan error)
rc.breakChannel = make(chan bool)
rc.breakResultChannel = make(chan error)
rc.unlockChannel = make(chan bool)
rc.unlockResponseChannel = make(chan bool)
rc.am.Init()
rc.am.AddAuthorization("none")
go rc.start()
}
// NewInboundConnection creates a new Connection struct
// modelling an Inbound Connection
func NewInboundConnection(conn io.ReadWriteCloser) *Connection {
rc := new(Connection)
rc.conn = conn
rc.IsInbound = true
rc.init()
rc.channelManager = NewServerChannelManager()
rc.ctrlChannel.Init(rc.channelManager)
return rc
}
// NewOutboundConnection creates a new Connection struct
// modelling an Inbound Connection
func NewOutboundConnection(conn io.ReadWriteCloser, remoteHostname string) *Connection {
rc := new(Connection)
rc.conn = conn
rc.IsInbound = false
rc.init()
rc.RemoteHostname = remoteHostname
rc.channelManager = NewClientChannelManager()
rc.ctrlChannel.Init(rc.channelManager)
return rc
}
// start
func (rc *Connection) start() {
for {
packet, err := rc.RecvRicochetPacket(rc.conn)
if err != nil {
rc.errorChannel <- err
return
}
rc.packetChannel <- packet
}
}
// Do allows any function utilizing Connection to be run safely, if you're
// careful. All operations which require access (directly or indirectly) to
// Connection while Process is running need to use Do. Calls to Do without
// Process running will block unless the connection is closed, which is
// returned as ConnectionClosedError.
//
// Like a mutex, Do cannot be called recursively. This will deadlock. As
// a result, no API in this library that can be reached from the application
// should use Do, with few exceptions. This would make the API impossible
// to use safely in many cases.
//
// Do is safe to call from methods of connection.Handler and channel.Handler
// that are called by Process.
func (rc *Connection) Do(do func() error) error {
// There's a complicated little dance here to prevent a race when the
// Process call is returning for a connection error. The problem is
// that if Do simply checked rc.closed and then tried to send, it's
// possible for Process to change rc.closed and stop reading before the
// send statement is executed, creating a deadlock.
//
// To prevent this, all of the functions that block on Process should
// do so by acquiring processBlockMutex, aborting if rc.closed is true,
// performing their blocking channel operations, and then releasing the
// mutex.
//
// This works because Process will always use a separate goroutine to
// acquire processBlockMutex before changing rc.closed, and the mutex
// guarantees that no blocking channel operation can happen during or
// after the value is changed. Since these operations block the Process
// loop, the behavior of multiple concurrent calls to Do/Break doesn't
// change: they just end up blocking on the mutex before blocking on the
// channel.
rc.processBlockMutex.Lock()
defer rc.processBlockMutex.Unlock()
if rc.closed {
return utils.ConnectionClosedError
}
// Force process to soft-break so we can lock
log.Debugln("request unlocking of process loop for do()")
rc.unlockChannel <- true
log.Debugln("process loop is unlocked for do()")
defer func() {
log.Debugln("giving up lock process loop after do() ")
rc.unlockResponseChannel <- true
}()
// Process sets rc.closing when it's trying to acquire the mutex and
// close down the connection. Behave as if the connection was already
// closed.
if rc.closing {
return utils.ConnectionClosedError
}
return do()
}
// DoContext behaves in the same way as Do, but also respects the provided
// context when blocked, and passes the context to the callback function.
//
// DoContext should be used when any call to Do may need to be cancelled
// or timed out.
func (rc *Connection) DoContext(ctx context.Context, do func(context.Context) error) error {
// .. see above
rc.processBlockMutex.Lock()
defer rc.processBlockMutex.Unlock()
if rc.closed {
return utils.ConnectionClosedError
}
// Force process to soft-break so we can lock
log.Debugln("request unlocking of process loop for do()")
select {
case rc.unlockChannel <- true:
break
case <-ctx.Done():
log.Debugln("giving up on unlocking process loop for do() because context cancelled")
return ctx.Err()
}
log.Debugln("process loop is unlocked for do()")
defer func() {
log.Debugln("giving up lock process loop after do() ")
rc.unlockResponseChannel <- true
}()
if rc.closing {
return utils.ConnectionClosedError
}
return do(ctx)
}
// RequestOpenChannel sends an OpenChannel message to the remote client.
// An error is returned only if the requirements for opening this channel
// are not met on the local side (a nil error return does not mean the
// channel was opened successfully, because channels open asynchronously).
func (rc *Connection) RequestOpenChannel(ctype string, handler channels.Handler) (*channels.Channel, error) {
log.Debugln(fmt.Sprintf("requesting open channel of type %s", ctype))
channel, err := rc.buildChannel(handler, rc.channelManager.OpenChannelRequest)
if err == nil {
response, err := handler.OpenOutbound(channel)
return rc.handleChannelOpening(channel, response, err)
}
return nil, err
}
func (rc *Connection) handleChannelOpening(channel *channels.Channel, response []byte, err error) (*channels.Channel, error) {
if err == nil {
rc.SendRicochetPacket(rc.conn, 0, response)
return channel, nil
}
log.Debugln(fmt.Sprintf("failed to request open channel: %v", err))
rc.channelManager.RemoveChannel(channel.ID)
return nil, err
}
func (rc *Connection) buildChannel(handler channels.Handler, openChannelFunc func(handler channels.Handler) (*channels.Channel, error)) (*channels.Channel, error) {
err := rc.am.Authorized(handler.RequiresAuthentication())
if err == nil {
channel, err := openChannelFunc(handler)
if err == nil {
channel.SendMessage = func(message []byte) {
rc.SendRicochetPacket(rc.conn, channel.ID, message)
}
channel.DelegateAuthorization = func() {
rc.am.AddAuthorization(handler.Type())
}
channel.CloseChannel = func() {
rc.SendRicochetPacket(rc.conn, channel.ID, []byte{})
rc.channelManager.RemoveChannel(channel.ID)
}
channel.DelegateEncryption = func(key [32]byte) {
rc.SetEncryptionKey(key)
}
return channel, nil
}
return nil, err
}
return nil, err
}
// processUserCallback should be used to wrap any calls into handlers or
// application code from the Process goroutine. It handles calls to Do
// from within that code to prevent deadlocks.
func (rc *Connection) processUserCallback(cb func()) {
done := make(chan struct{})
go func() {
defer close(done)
cb()
}()
for {
select {
case <-done:
return
case <-rc.unlockChannel:
<-rc.unlockResponseChannel
}
}
}
// Process receives socket and protocol events for the connection. Methods
// of the application-provided `handler` will be called from this goroutine
// for all events.
//
// Process must be running in order to handle any events on the connection,
// including connection close.
//
// Process blocks until the connection is closed or until Break() is called.
// Infof the connection is closed, a non-nil error is returned.
func (rc *Connection) Process(handler Handler) error {
if rc.closed {
return utils.ConnectionClosedError
}
log.Debugln("entering process loop")
rc.processUserCallback(func() { handler.OnReady(rc) })
// There are exactly two ways out of this loop: a signal on breakChannel
// caused by a call to Break, or a connection-fatal error on errorChannel.
//
// In the Break case, no particular care is necessary; it is the caller's
// responsibility to make sure there aren't e.g. concurrent calls to Do.
//
// Because connection errors can happen spontaneously, they must carefully
// prevent concurrent calls to Break or Do that could deadlock when Process
// returns.
for {
var packet utils.RicochetData
select {
case <-rc.unlockChannel:
<-rc.unlockResponseChannel
continue
case <-rc.breakChannel:
log.Debugln("process has ended after break")
rc.breakResultChannel <- nil
return nil
case packet = <-rc.packetChannel:
break
case err := <-rc.errorChannel:
rc.conn.Close()
rc.closing = true
// In order to safely close down concurrent calls to Do or Break,
// processBlockMutex must be held before setting rc.closed. That cannot
// happen in this goroutine, because one of those calls may already hold
// the mutex and be blocking on a channel send to this method. So the
// process here is to have a goroutine acquire the lock, set rc.closed, and
// signal back. Meanwhile, this one keeps handling unlockChannel and
// breakChannel.
closedChan := make(chan struct{})
go func() {
rc.processBlockMutex.Lock()
defer rc.processBlockMutex.Unlock()
rc.closingLock.Lock()
defer rc.closingLock.Unlock()
rc.closed = true
close(closedChan)
}()
// Keep accepting calls from Do or Break until closedChan signals that they're
// safely shut down.
clearLoop:
for {
select {
case <-rc.unlockChannel:
<-rc.unlockResponseChannel
case <-rc.breakChannel:
rc.breakResultChannel <- utils.ConnectionClosedError
case <-closedChan:
break clearLoop
}
}
// This is the one case where processUserCallback isn't necessary, because
// all calls to Do immediately return ConnectionClosedError now.
handler.OnClosed(err)
return err
}
if packet.Channel == 0 {
log.Debugln(fmt.Sprintf("received control packet on channel %d", packet.Channel))
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(packet.Data[:], res)
if err == nil {
// Wrap controlPacket in processUserCallback, since it calls out in many
// places, and wrapping the rest is harmless.
rc.processUserCallback(func() { rc.controlPacket(handler, res) })
}
} else {
// Let's check to see if we have defined this channel.
channel, found := rc.channelManager.GetChannel(packet.Channel)
if found {
if len(packet.Data) == 0 {
log.Debugln(fmt.Sprintf("removing channel %d", packet.Channel))
rc.channelManager.RemoveChannel(packet.Channel)
rc.processUserCallback(func() { channel.Handler.Closed(utils.ChannelClosedByPeerError) })
} else {
log.Debugln(fmt.Sprintf("received packet on %v channel %d", channel.Handler.Type(), packet.Channel))
// Send The Ricochet Packet to the Handler
rc.processUserCallback(func() { channel.Handler.Packet(packet.Data[:]) })
}
} else {
// When a non-zero packet is received for an unknown
// channel, the recipient responds by closing
// that channel.
log.Debugln(fmt.Sprintf("received packet on unknown channel %d. closing.", packet.Channel))
if len(packet.Data) != 0 {
rc.SendRicochetPacket(rc.conn, packet.Channel, []byte{})
}
}
}
}
}
func (rc *Connection) controlPacket(handler Handler, res *Protocol_Data_Control.Packet) {
if res.GetOpenChannel() != nil {
opm := res.GetOpenChannel()
chandler, err := handler.OnOpenChannelRequest(opm.GetChannelType())
if err == nil {
openChannel := func(chandler channels.Handler) (*channels.Channel, error) {
return rc.channelManager.OpenChannelRequestFromPeer(opm.GetChannelIdentifier(), chandler)
}
channel, err := rc.buildChannel(chandler, openChannel)
if err != nil {
log.Errorf("Failed to build channel from Control Packet: %v", err)
return
}
response, err := chandler.OpenInbound(channel, opm)
_, err = rc.handleChannelOpening(channel, response, err)
if err != nil {
rc.SendRicochetPacket(rc.conn, 0, []byte{})
}
return
}
errorText := "GenericError"
switch err {
case utils.UnknownChannelTypeError:
errorText = "UnknownTypeError"
case utils.UnauthorizedChannelTypeError:
errorText = "UnauthorizedTypeError"
}
// Send Error Packet
response := rc.messageBuilder.RejectOpenChannel(opm.GetChannelIdentifier(), errorText)
log.Debugln(fmt.Sprintf("sending reject open channel for %v: %v", opm.GetChannelIdentifier(), errorText))
rc.SendRicochetPacket(rc.conn, 0, response)
} else if res.GetChannelResult() != nil {
rc.ctrlChannel.ProcessChannelResult(res.GetChannelResult())
} else if res.GetKeepAlive() != nil {
// XXX Though not currently part of the protocol
// We should likely put these calls behind
// authentication.
log.Debugln("received keep alive packet")
respond, data := rc.ctrlChannel.ProcessKeepAlive(res.GetKeepAlive())
if respond {
log.Debugln("sending keep alive response")
rc.SendRicochetPacket(rc.conn, 0, data)
}
} else if res.GetEnableFeatures() != nil {
log.Debugln("received enable features packet")
data := rc.ctrlChannel.ProcessEnableFeatures(handler, res.GetEnableFeatures())
log.Debugln(fmt.Sprintf("sending featured enabled: %v", data))
rc.SendRicochetPacket(rc.conn, 0, data)
} else if res.GetFeaturesEnabled() != nil {
rc.SupportChannels = res.GetFeaturesEnabled().GetFeature()
log.Debugln(fmt.Sprintf("connection supports: %v", rc.SupportChannels))
}
}
// EnableFeatures sends an EnableFeatures messages which includes the
// list of `features`
func (rc *Connection) EnableFeatures(features []string) {
messageBuilder := new(utils.MessageBuilder)
raw := messageBuilder.EnableFeatures(features)
rc.SendRicochetPacket(rc.conn, 0, raw)
}
// Break causes Process() to return, but does not close the underlying connection
// Break returns an error if it would not be valid to call Process() again for
// the connection now. Currently, the only such error is ConnectionClosedError.
func (rc *Connection) Break() error {
// See Do() for an explanation of the concurrency here; it's complicated.
// The summary is that this mutex prevents races on connection close that
// could lead to deadlocks in Block().
rc.processBlockMutex.Lock()
defer rc.processBlockMutex.Unlock()
if rc.closed {
log.Debugln("ignoring break because connection is already closed")
return utils.ConnectionClosedError
}
log.Debugln("breaking out of process loop")
rc.breakChannel <- true
return <-rc.breakResultChannel // Wait for Process to End
}
// Channel is a convienciance method for returning a given channel to the caller
// of Process() - TODO - this is kind of ugly.
func (rc *Connection) Channel(ctype string, way channels.Direction) *channels.Channel {
return rc.channelManager.Channel(ctype, way)
}
// Close tearsdown a Process() and explicitly Closes the connection.
// Note, that if Process() is holding a connection this will trigger an Error
func (rc *Connection) Close() {
// Kill the Ricochet Connection.
log.Debugf("Closing Ricochet Connection for %v", rc.RemoteHostname)
rc.conn.Close()
rc.closingLock.Lock()
rc.closed = true
rc.closingLock.Unlock()
}

View File

@ -0,0 +1,136 @@
package connection
import (
"crypto/rand"
"crypto/rsa"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"net"
"testing"
"time"
)
// Server
func ServerAuthValid(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
return true, true
}
// Server
func ServerAuthValid3DH(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) {
return true, true
}
func TestProcessAuthAs3DHServer(t *testing.T) {
ln, _ := net.Listen("tcp", "127.0.0.1:0")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
go func() {
cconn, _ := net.Dial("tcp", ln.Addr().String())
cpub, cpriv, _ := ed25519.GenerateKey(rand.Reader)
hostname := utils.GetTorV3Hostname(pub)
orc := NewOutboundConnection(cconn, hostname)
known, err := HandleOutboundConnection(orc).ProcessAuthAsV3Client(identity.InitializeV3("", &cpriv, &cpub))
if err != nil {
t.Errorf("Error while testing ProcessAuthAsClient (in ProcessAuthAsServer) %v", err)
return
} else if !known {
t.Errorf("Client should have been known to the server, instead known was: %v", known)
return
}
}()
conn, _ := ln.Accept()
rc := NewInboundConnection(conn)
err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH)
if err != nil {
t.Errorf("Error while testing ProcessAuthAsServer: %v", err)
}
// Wait for server to finish
time.Sleep(time.Second * 2)
// Test Close
rc.Close()
}
func TestProcessAuthAsV3ServerFail(t *testing.T) {
ln, _ := net.Listen("tcp", "127.0.0.1:0")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
go func() {
cconn, _ := net.Dial("tcp", ln.Addr().String())
cpub, cpriv, _ := ed25519.GenerateKey(rand.Reader)
// Setting the RemoteHostname to the client pub key approximates a server sending the wrong public key.
hostname := utils.GetTorV3Hostname(cpub)
orc := NewOutboundConnection(cconn, hostname)
HandleOutboundConnection(orc).ProcessAuthAsV3Client(identity.InitializeV3("", &cpriv, &cpub))
}()
conn, _ := ln.Accept()
rc := NewInboundConnection(conn)
err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH)
if err == nil {
t.Errorf("Error while testing ProcessAuthAsServer: %v", err)
}
}
func TestProcessAuthAsV3ClientFail(t *testing.T) {
ln, _ := net.Listen("tcp", "127.0.0.1:0")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
go func() {
cconn, _ := net.Dial("tcp", ln.Addr().String())
// Giving the client inconsistent keypair to make EDH fail
cpub, _, _ := ed25519.GenerateKey(rand.Reader)
_, cpriv, _ := ed25519.GenerateKey(rand.Reader)
hostname := utils.GetTorV3Hostname(pub)
orc := NewOutboundConnection(cconn, hostname)
HandleOutboundConnection(orc).ProcessAuthAsV3Client(identity.InitializeV3("", &cpriv, &cpub))
}()
conn, _ := ln.Accept()
rc := NewInboundConnection(conn)
err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH)
if err == nil {
t.Errorf("Error while testing ProcessAuthAsServer: %v", err)
}
}
func TestProcessAuthTimeout(t *testing.T) {
ln, _ := net.Listen("tcp", "127.0.0.1:0")
go func() {
net.Dial("tcp", ln.Addr().String())
time.Sleep(17 * time.Second)
}()
conn, _ := ln.Accept()
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
rc := NewInboundConnection(conn)
err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH)
if err != utils.ActionTimedOutError {
t.Errorf("Error while testing TestProcessAuthTimeout - Should have timed out after 15 seconds, instead ERR was %v", err)
}
}

View File

@ -0,0 +1,65 @@
package connection
import (
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
)
// ControlChannel encapsulates logic for the control channel processing
type ControlChannel struct {
channelManager *ChannelManager
}
// Init sets up a control channel
func (ctrl *ControlChannel) Init(channelManager *ChannelManager) {
ctrl.channelManager = channelManager
}
// ProcessChannelResult contains the logic for processing a channelresult message
func (ctrl *ControlChannel) ProcessChannelResult(cr *Protocol_Data_Control.ChannelResult) (bool, error) {
id := cr.GetChannelIdentifier()
channel, found := ctrl.channelManager.GetChannel(id)
if !found {
return false, utils.UnexpectedChannelResultError
}
if cr.GetOpened() {
log.Debugln(fmt.Sprintf("channel of type %v opened on %v", channel.Type, id))
channel.Handler.OpenOutboundResult(nil, cr)
return true, nil
}
log.Debugln(fmt.Sprintf("channel of type %v rejected on %v", channel.Type, id))
channel.Handler.OpenOutboundResult(errors.New(cr.GetCommonError().String()), cr)
return false, nil
}
// ProcessKeepAlive contains logic for responding to keep alives
func (ctrl *ControlChannel) ProcessKeepAlive(ka *Protocol_Data_Control.KeepAlive) (bool, []byte) {
if ka.GetResponseRequested() {
messageBuilder := new(utils.MessageBuilder)
return true, messageBuilder.KeepAlive(true)
}
return false, nil
}
// ProcessEnableFeatures correctly handles a features enabled packet
func (ctrl *ControlChannel) ProcessEnableFeatures(handler Handler, ef *Protocol_Data_Control.EnableFeatures) []byte {
featuresToEnable := ef.GetFeature()
supportChannels := handler.GetSupportedChannelTypes()
result := []string{}
for _, v := range featuresToEnable {
for _, s := range supportChannels {
if v == s {
result = append(result, v)
}
}
}
messageBuilder := new(utils.MessageBuilder)
raw := messageBuilder.FeaturesEnabled(result)
return raw
}

View File

@ -0,0 +1,105 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
)
type MockHandler struct {
AutoConnectionHandler
}
func (m *MockHandler) GetSupportedChannelTypes() []string {
return []string{"im.ricochet.chat"}
}
func TestChannelResultNotOpened(t *testing.T) {
ccm := NewClientChannelManager()
ctrlChannel := new(ControlChannel)
ctrlChannel.Init(ccm)
chatChannel := new(channels.ChatChannel)
_, err := ccm.OpenChannelRequestFromPeer(2, chatChannel)
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(2),
Opened: proto.Bool(false),
}
opened, err := ctrlChannel.ProcessChannelResult(cr)
if opened != false || err != nil {
t.Errorf("ProcessChannelResult should have resulted in n channel being opened, and no error %v %v", opened, err)
}
}
func TestChannelResultError(t *testing.T) {
ccm := NewClientChannelManager()
ctrlChannel := new(ControlChannel)
ctrlChannel.Init(ccm)
chatChannel := new(channels.ChatChannel)
_, err := ccm.OpenChannelRequestFromPeer(2, chatChannel)
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(3),
Opened: proto.Bool(false),
}
opened, err := ctrlChannel.ProcessChannelResult(cr)
if opened != false || err != utils.UnexpectedChannelResultError {
t.Errorf("ProcessChannelResult should have resulted in n channel being opened, and an error %v %v", opened, err)
}
}
func TestKeepAliveNoResponse(t *testing.T) {
ctrlChannel := new(ControlChannel)
ka := &Protocol_Data_Control.KeepAlive{
ResponseRequested: proto.Bool(false),
}
respond, _ := ctrlChannel.ProcessKeepAlive(ka)
if respond == true {
t.Errorf("KeepAlive process should have not needed a response %v %v", ka, respond)
}
}
func TestKeepAliveRequestResponse(t *testing.T) {
ka := &Protocol_Data_Control.KeepAlive{
ResponseRequested: proto.Bool(true),
}
ctrlChannel := new(ControlChannel)
respond, _ := ctrlChannel.ProcessKeepAlive(ka)
if respond == false {
t.Errorf("KeepAlive process should have produced a response %v %v", ka, respond)
}
}
func TestEnableFeatures(t *testing.T) {
handler := new(MockHandler)
features := []string{"feature1", "im.ricochet.chat"}
ef := &Protocol_Data_Control.EnableFeatures{
Feature: features,
}
ctrlChannel := new(ControlChannel)
raw := ctrlChannel.ProcessEnableFeatures(handler, ef)
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(raw, res)
if err != nil || res.GetFeaturesEnabled() == nil {
t.Errorf("Decoding FeaturesEnabled Packet failed: %v %v", err, res)
}
if len(res.GetFeaturesEnabled().GetFeature()) != 1 {
t.Errorf("Decoding FeaturesEnabled Errored, unexpected length %v", res.GetFeaturesEnabled().GetFeature())
}
if res.GetFeaturesEnabled().GetFeature()[0] != "im.ricochet.chat" {
t.Errorf("Decoding FeaturesEnabled Errored, unexpected feature enabled %v ", res.GetFeaturesEnabled().GetFeature()[0])
}
}

30
connection/handler.go Normal file
View File

@ -0,0 +1,30 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
)
// Handler reacts to low-level events on a protocol connection.
// There should be a unique instance of a ConnectionHandler type per
// OpenConnection.
type Handler interface {
// OnReady is called when the connection begins using this handler.
OnReady(oc *Connection)
// OnClosed is called when the OpenConnection has closed for any reason.
OnClosed(err error)
// OpenChannelRequest is called when the peer asks to open a channel of
// `type`. `raw` contains the protocol OpenChannel message including any
// extension data. If this channel type is recognized and allowed by this
// connection in this state, return a type implementing ChannelHandler for
// events related to this channel. Returning an error or nil rejects the
// channel.
//
// Channel type handlers may implement additional state and sanity checks.
// A non-nil return from this function does not guarantee that the channel
// will be opened.
OnOpenChannelRequest(ctype string) (channels.Handler, error)
GetSupportedChannelTypes() []string
}

View File

@ -0,0 +1,87 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/inbound"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/policies"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"sync"
)
// InboundConnectionHandler is a convieniance wrapper for handling inbound
// connections
type InboundConnectionHandler struct {
connection *Connection
}
// HandleInboundConnection returns an InboundConnectionHandler given a connection
func HandleInboundConnection(c *Connection) *InboundConnectionHandler {
ich := new(InboundConnectionHandler)
ich.connection = c
return ich
}
// ProcessAuthAsV3Server blocks until authentication has succeeded, failed, or the
// connection is closed. A non-nil error is returned in all cases other than successful
// and accepted authentication.
//
// ProcessAuthAsV3Server cannot be called at the same time as any other call to a Process
// function. Another Process function must be called after this function successfully
// returns to continue handling connection events.
//
// The acceptCallback function is called after receiving a valid authentication proof
// with the client's authenticated hostname and public key. acceptCallback must return
// true to accept authentication and allow the connection to continue, and also returns a
// boolean indicating whether the contact is known and recognized. Unknown contacts will
// assume they are required to send a contact request before any other activity.
func (ich *InboundConnectionHandler) ProcessAuthAsV3Server(v3identity identity.Identity, sach func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool)) error {
var breakOnce sync.Once
var authAllowed, authKnown bool
var authHostname string
onAuthValid := func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) {
authAllowed, authKnown = sach(hostname, publicKey)
if authAllowed {
authHostname = hostname
}
breakOnce.Do(func() { go ich.connection.Break() })
return authAllowed, authKnown
}
onAuthInvalid := func(err error) {
// err is ignored at the moment
breakOnce.Do(func() { go ich.connection.Break() })
}
ach := new(AutoConnectionHandler)
ach.Init()
ach.RegisterChannelHandler("im.ricochet.auth.3dh",
func() channels.Handler {
return &inbound.Server3DHAuthChannel{
ServerIdentity: v3identity,
ServerAuthValid: onAuthValid,
ServerAuthInvalid: onAuthInvalid,
}
})
// Ensure that the call to Process() cannot outlive this function,
// particularly for the case where the policy timeout expires
defer breakOnce.Do(func() { ich.connection.Break() })
policy := policies.UnknownPurposeTimeout
err := policy.ExecuteAction(func() error {
return ich.connection.Process(ach)
})
if err == nil {
if authAllowed == true {
ich.connection.RemoteHostname = authHostname
return nil
}
return utils.ClientFailedToAuthenticateError
}
return err
}

View File

@ -0,0 +1,85 @@
package connection
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/outbound"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/policies"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"sync"
)
// OutboundConnectionHandler is a convieniance wrapper for handling outbound
// connections
type OutboundConnectionHandler struct {
connection *Connection
}
// HandleOutboundConnection returns an OutboundConnectionHandler given a connection
func HandleOutboundConnection(c *Connection) *OutboundConnectionHandler {
och := new(OutboundConnectionHandler)
och.connection = c
return och
}
// ProcessAuthAsV3Client blocks until authentication has succeeded or failed with the
// provided identity, or the connection is closed. A non-nil error is returned in all
// cases other than successful authentication.
//
// ProcessAuthAsV3Client cannot be called at the same time as any other call to a Process
// function. Another Process function must be called after this function successfully
// returns to continue handling connection events.
//
// For successful authentication, the `known` return value indicates whether the peer
// accepts us as a known contact. Unknown contacts will generally need to send a contact
// request before any other activity.
func (och *OutboundConnectionHandler) ProcessAuthAsV3Client(v3identity identity.Identity) (bool, error) {
ach := new(AutoConnectionHandler)
ach.Init()
// Make sure that calls to Break in this function cannot race
var breakOnce sync.Once
var accepted, isKnownContact bool
authCallback := func(accept, known bool) {
accepted = accept
isKnownContact = known
// Cause the Process() call below to return.
// If Break() is called from here, it _must_ use go, because this will
// execute in the Process goroutine, and Break() will deadlock.
breakOnce.Do(func() { go och.connection.Break() })
}
processResult := make(chan error, 1)
go func() {
// Break Process() if timed out; no-op if Process returned a conn error
defer func() { breakOnce.Do(func() { och.connection.Break() }) }()
policy := policies.UnknownPurposeTimeout
err := policy.ExecuteAction(func() error {
return och.connection.Process(ach)
})
processResult <- err
}()
err := och.connection.Do(func() error {
_, err := och.connection.RequestOpenChannel("im.ricochet.auth.3dh",
&outbound.Client3DHAuthChannel{
ClientIdentity: v3identity,
ServerHostname: och.connection.RemoteHostname,
ClientAuthResult: authCallback,
})
return err
})
if err != nil {
breakOnce.Do(func() { och.connection.Break() })
return false, err
}
if err = <-processResult; err != nil {
return false, err
}
if accepted == true {
return isKnownContact, nil
}
return false, utils.ServerRejectedClientConnectionError
}

46
connectivity/acn.go Normal file
View File

@ -0,0 +1,46 @@
package connectivity
import (
"net"
)
// PrivateKey represents a private key using an unspecified algorithm.
type PrivateKey interface{}
// ListenService is an address that was opened with Listen() and can Accept() new connections
type ListenService interface {
// AddressIdentity is the core "identity" part of an address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd
AddressIdentity() string
// AddressFull is the full network address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd.onion:9878
AddressFull() string
Accept() (net.Conn, error)
Close()
}
// ACN is Anonymous Communication Network implementation wrapper that supports Open for new connections and Listen to accept connections
type ACN interface {
// GetBootstrapStatus returns an int 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message
GetBootstrapStatus() (int, string)
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
WaitTillBootstrapped()
// Sets the calback function to be called when ACN status changes
SetStatusCallback(callback func(int, string))
// Restarts the underlying connection
Restart()
// Open takes a hostname and returns a net.conn to the derived endpoint
// Open allows a client to resolve various hostnames to connections
// The supported types are onions address are:
// * ricochet:jlq67qzo6s4yp3sp
// * jlq67qzo6s4yp3sp
// * 127.0.0.1:55555|jlq67qzo6s4yp3sp - Localhost Connection
Open(hostname string) (net.Conn, string, error)
// Listen takes a private key and a port and returns a ListenService for it
Listen(identity PrivateKey, port int) (ListenService, error)
Close()
}

View File

@ -0,0 +1,77 @@
package connectivity
import (
"fmt"
"net"
"strings"
)
type localListenService struct {
l net.Listener
}
type localProvider struct {
}
func (ls *localListenService) AddressFull() string {
return ls.l.Addr().String()
}
func (ls *localListenService) AddressIdentity() string {
return ls.l.Addr().String()
}
func (ls *localListenService) Accept() (net.Conn, error) {
return ls.l.Accept()
}
func (ls *localListenService) Close() {
ls.l.Close()
}
// GetBootstrapStatus returns an int 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message
func (lp *localProvider) GetBootstrapStatus() (int, string) {
return 100, "Done"
}
func (lp *localProvider) SetStatusCallback(callback func(int, string)) {
// nop
}
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (lp *localProvider) WaitTillBootstrapped() {
}
func (lp *localProvider) Listen(identity PrivateKey, port int) (ListenService, error) {
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port))
return &localListenService{l}, err
}
func (lp *localProvider) Open(hostname string) (net.Conn, string, error) {
// Localhost (127.0.0.1:55555|jlq67qzo6s4yp3sp) for testing
addrParts := strings.Split(hostname, "|")
tcpAddr, err := net.ResolveTCPAddr("tcp", addrParts[0])
if err != nil {
return nil, "", CannotResolveLocalTCPAddressError
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
return nil, "", CannotDialLocalTCPAddressError
}
// return just the onion address, not the local override for the hostname
return conn, addrParts[1], nil
}
func (lp *localProvider) Restart() {
//noop
}
func (lp *localProvider) Close() {
}
// LocalProvider returns a for testing use only local clearnet implementation of a ACN interface
func LocalProvider() ACN {
return &localProvider{}
}

View File

@ -0,0 +1,9 @@
// +build !windows
package connectivity
import (
"syscall"
)
var sysProcAttr = &syscall.SysProcAttr{}

View File

@ -0,0 +1,9 @@
// +build windows
package connectivity
import (
"syscall"
)
var sysProcAttr = &syscall.SysProcAttr{HideWindow: true}

382
connectivity/torProvider.go Normal file
View File

@ -0,0 +1,382 @@
package connectivity
import (
"context"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"github.com/cretz/bine/control"
"github.com/cretz/bine/process"
"github.com/cretz/bine/tor"
bineed255192 "github.com/cretz/bine/torutil/ed25519"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
"net"
"net/textproto"
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
const (
// CannotResolveLocalTCPAddressError is thrown when a local ricochet connection has the wrong format.
CannotResolveLocalTCPAddressError = utils.Error("CannotResolveLocalTCPAddressError")
// CannotDialLocalTCPAddressError is thrown when a connection to a local ricochet address fails.
CannotDialLocalTCPAddressError = utils.Error("CannotDialLocalTCPAddressError")
// CannotDialRicochetAddressError is thrown when a connection to a ricochet address fails.
CannotDialRicochetAddressError = utils.Error("CannotDialRicochetAddressError")
)
const (
minStatusIntervalMs = 200
maxStatusIntervalMs = 2000
)
type onionListenService struct {
os *tor.OnionService
tp *torProvider
}
type torProvider struct {
t *tor.Tor
dialer *tor.Dialer
appDirectory string
bundeledTorPath string
lock sync.Mutex
breakChan chan bool
childListeners map[string]*onionListenService
statusCallback func(int, string)
}
func (ols *onionListenService) AddressFull() string {
return ols.os.Addr().String()
}
func (ols *onionListenService) AddressIdentity() string {
return ols.os.Addr().String()[:56]
}
func (ols *onionListenService) Accept() (net.Conn, error) {
return ols.os.Accept()
}
func (ols *onionListenService) Close() {
ols.tp.unregisterListener(ols.AddressIdentity())
ols.os.Close()
}
// GetBootstrapStatus returns an int -1 on error or 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message
func (tp *torProvider) GetBootstrapStatus() (int, string) {
if tp.t == nil {
return -1, "error: no tor, trying to restart..."
}
kvs, err := tp.t.Control.GetInfo("status/bootstrap-phase")
if err != nil {
return -1, "error"
}
progress := 0
status := ""
if len(kvs) > 0 {
progRe := regexp.MustCompile("PROGRESS=([0-9]*)")
sumRe := regexp.MustCompile("SUMMARY=\"(.*)\"$")
if progMatches := progRe.FindStringSubmatch(kvs[0].Val); len(progMatches) > 1 {
progress, _ = strconv.Atoi(progMatches[1])
}
if statusMatches := sumRe.FindStringSubmatch(kvs[0].Val); len(statusMatches) > 1 {
status = statusMatches[1]
}
}
return progress, status
}
// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (tp *torProvider) WaitTillBootstrapped() {
for true {
progress, _ := tp.GetBootstrapStatus()
if progress == 100 {
break
}
time.Sleep(100 * time.Millisecond)
}
}
func (tp *torProvider) Listen(identity PrivateKey, port int) (ListenService, error) {
var onion = ""
var privkey ed25519.PrivateKey
tp.lock.Lock()
defer tp.lock.Unlock()
if tp.t == nil {
return nil, errors.New("Tor Provider closed")
}
switch pk := identity.(type) {
case ed25519.PrivateKey:
privkey = pk
gpubk := pk.Public()
switch pubk := gpubk.(type) {
case ed25519.PublicKey:
onion = utils.GetTorV3Hostname(pubk)
}
}
// Hack around tor detached onions not having a more obvious resume mechanism
// So we use deterministic ports
seedbytes := sha3.New224().Sum([]byte(onion))
localport := int(seedbytes[0]) + (int(seedbytes[1]) << 8)
if localport < 1024 { // this is not uniformly random, but we don't need it to be
localport += 1024
}
localListener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport))
conf := &tor.ListenConf{NoWait: true, Version3: true, Key: identity, RemotePorts: []int{port}, Detach: true, DiscardKey: true, LocalListener: localListener}
os, err := tp.t.Listen(nil, conf)
if err != nil && strings.Contains(err.Error(), "550 Unspecified Tor error: Onion address collision") {
os = &tor.OnionService{Tor: tp.t, LocalListener: localListener, ID: onion, Version3: true, Key: bineed255192.FromCryptoPrivateKey(privkey), ClientAuths: make(map[string]string, 0), RemotePorts: []int{port}}
err = nil
}
// Not set in t.Listen if supplied, we want it to handle this however
os.CloseLocalListenerOnClose = true
if err != nil {
return nil, err
}
ols := &onionListenService{os: os, tp: tp}
tp.childListeners[ols.AddressIdentity()] = ols
return ols, nil
}
func (tp *torProvider) Restart() {
if tp.statusCallback != nil {
tp.statusCallback(0, "rebooting")
}
tp.restart()
}
func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
tp.lock.Lock()
if tp.t == nil {
tp.lock.Unlock()
return nil, hostname, errors.New("Tor is offline")
}
tp.lock.Unlock()
resolvedHostname := hostname
if strings.HasPrefix(hostname, "ricochet:") {
addrParts := strings.Split(hostname, ":")
resolvedHostname = addrParts[1]
}
conn, err := tp.dialer.Dial("tcp", resolvedHostname+".onion:9878")
return conn, resolvedHostname, err
}
func (tp *torProvider) Close() {
for _, child := range tp.childListeners {
child.Close()
}
tp.lock.Lock()
defer tp.lock.Unlock()
tp.breakChan <- true
if tp.t != nil {
tp.t.Close()
tp.t = nil
}
}
func (tp *torProvider) SetStatusCallback(callback func(int, string)) {
tp.lock.Lock()
defer tp.lock.Unlock()
tp.statusCallback = callback
}
// StartTor creates/starts a Tor ACN and returns a usable ACN object
func StartTor(appDirectory string, bundledTorPath string) (ACN, error) {
tp, err := startTor(appDirectory, bundledTorPath)
if err == nil {
tp.dialer, err = tp.t.Dialer(nil, &tor.DialConf{})
if err == nil {
go tp.monitorRestart()
}
}
return tp, err
}
// newHideCmd creates a Creator function for bine which generates a cmd that one windows will hide the dosbox
func newHideCmd(exePath string) process.Creator {
return process.CmdCreatorFunc(func(ctx context.Context, args ...string) (*exec.Cmd, error) {
cmd := exec.CommandContext(ctx, exePath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = sysProcAttr
return cmd, nil
})
}
func startTor(appDirectory string, bundledTorPath string) (*torProvider, error) {
dataDir := path.Join(appDirectory, "tor")
os.MkdirAll(dataDir, 0700)
tp := &torProvider{appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool), statusCallback: nil}
// attempt connect to system tor
log.Debugf("dialing system tor control port\n")
controlport, err := dialControlPort(9051)
if err == nil {
// TODO: configurable auth
err := controlport.Authenticate("")
if err == nil {
log.Debugln("connected to control port")
pinfo, err := controlport.ProtocolInfo()
if err == nil && minTorVersionReqs(pinfo.TorVersion) {
log.Debugln("OK version " + pinfo.TorVersion)
tp.t = createFromExisting(controlport, dataDir)
return tp, nil
}
controlport.Close()
}
}
// if not, try running system tor
if checkCmdlineTorVersion("tor") {
t, err := tor.Start(nil, &tor.StartConf{EnableNetwork: true, DataDir: dataDir, DebugWriter: nil, ProcessCreator: newHideCmd("tor")})
if err == nil {
tp.t = t
return tp, nil
}
log.Debugf("Error connecting to self-run system tor: %v\n", err)
}
// try running bundledTor
if bundledTorPath != "" && checkCmdlineTorVersion(bundledTorPath) {
log.Debugln("using bundled tor '" + bundledTorPath + "'")
t, err := tor.Start(nil, &tor.StartConf{EnableNetwork: true, DataDir: dataDir, ExePath: bundledTorPath, DebugWriter: nil, ProcessCreator: newHideCmd(bundledTorPath)})
if err != nil {
log.Debugf("Error running bundled tor: %v\n", err)
}
tp.t = t
return tp, err
}
return nil, errors.New("Could not connect to or start Tor that met requirments (min Tor version 0.3.5.x)")
}
func (tp *torProvider) unregisterListener(id string) {
tp.lock.Lock()
defer tp.lock.Unlock()
delete(tp.childListeners, id)
}
func (tp *torProvider) monitorRestart() {
lastBootstrapProgress := 0
interval := minStatusIntervalMs
for {
select {
case <-time.After(time.Millisecond * time.Duration(interval)):
prog, status := tp.GetBootstrapStatus()
if prog == -1 && tp.t != nil {
if tp.statusCallback != nil {
tp.statusCallback(prog, status)
}
tp.restart()
interval = minStatusIntervalMs
} else if prog != lastBootstrapProgress {
if tp.statusCallback != nil {
tp.statusCallback(prog, status)
}
interval = minStatusIntervalMs
} else {
if interval < maxStatusIntervalMs {
interval *= 2
}
}
lastBootstrapProgress = prog
case <-tp.breakChan:
return
}
}
}
func (tp *torProvider) restart() {
for _, child := range tp.childListeners {
child.Close()
}
tp.lock.Lock()
defer tp.lock.Unlock()
tp.t.Close()
tp.t = nil
for {
newTp, err := startTor(tp.appDirectory, tp.bundeledTorPath)
if err == nil {
tp.t = newTp.t
tp.dialer, _ = tp.t.Dialer(nil, &tor.DialConf{})
return
}
}
}
func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor {
t := &tor.Tor{
Process: nil,
Control: controlport,
ProcessCancelFunc: nil,
DataDir: datadir,
DeleteDataDirOnClose: false,
DebugWriter: nil,
StopProcessOnClose: false,
GeoIPCreatedFile: "",
GeoIPv6CreatedFile: "",
}
t.Control.DebugWriter = t.DebugWriter
t.EnableNetwork(nil, true)
return t
}
func checkCmdlineTorVersion(torCmd string) bool {
cmd := exec.Command(torCmd, "--version")
cmd.SysProcAttr = sysProcAttr
out, err := cmd.CombinedOutput()
re := regexp.MustCompile("[0-1]\\.[0-9]\\.[0-9]\\.[0-9]")
sysTorVersion := re.Find(out)
log.Infoln("tor version: " + string(sysTorVersion))
return err == nil && minTorVersionReqs(string(sysTorVersion))
}
// returns true if supplied version meets our min requirments
// min requirement: 0.3.5.x
func minTorVersionReqs(torversion string) bool {
torversions := strings.Split(torversion, ".") //eg: 0.3.4.8 or 0.3.5.1-alpha
log.Debugf("torversions: %v", torversions)
tva, _ := strconv.Atoi(torversions[0])
tvb, _ := strconv.Atoi(torversions[1])
tvc, _ := strconv.Atoi(torversions[2])
return tva > 0 || (tva == 0 && (tvb > 3 || (tvb == 3 && tvc >= 5)))
}
func dialControlPort(port int) (*control.Conn, error) {
textConn, err := textproto.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port))
if err != nil {
return nil, err
}
return control.NewConn(textConn), nil
}

View File

@ -0,0 +1,30 @@
package connectivity
import (
"fmt"
"testing"
)
func getStatusCallback(progChan chan int) func(int, string) {
return func(prog int, status string) {
fmt.Printf("%v %v\n", prog, status)
progChan <- prog
}
}
func TestTorProvider(t *testing.T) {
progChan := make(chan int)
acn, err := StartTor(".", "")
if err != nil {
t.Error(err)
return
}
acn.SetStatusCallback(getStatusCallback(progChan))
progress := 0
for progress < 100 {
progress = <-progChan
}
acn.Close()
}

View File

@ -1,41 +0,0 @@
package main
import (
"github.com/s-rah/go-ricochet"
"log"
)
// EchoBotService is an example service which simply echoes back what a client
// sends it.
type EchoBotService struct {
goricochet.StandardRicochetService
}
// IsKnownContact is configured to always accept Contact Requests
func (ebs *EchoBotService) IsKnownContact(hostname string) bool {
return true
}
// OnContactRequest - we always accept new contact request.
func (ebs *EchoBotService) OnContactRequest(oc *goricochet.OpenConnection, channelID int32, nick string, message string) {
ts.StandardRicochetService.OnContactRequest(oc, channelID, nick, message)
oc.AckContactRequestOnResponse(channelID, "Accepted")
oc.CloseChannel(channelID)
}
// OnChatMessage we acknowledge the message, grab the message content and send it back - opening
// a new channel if necessary.
func (ebs *EchoBotService) OnChatMessage(oc *goricochet.OpenConnection, channelID int32, messageID int32, message string) {
log.Printf("Received Message from %s: %s", oc.OtherHostname, message)
oc.AckChatMessage(channelID, messageID)
if oc.GetChannelType(6) == "none" {
oc.OpenChatChannel(6)
}
oc.SendMessage(6, message)
}
func main() {
ricochetService := new(EchoBotService)
ricochetService.Init("./private_key")
ricochetService.Listen(ricochetService, 12345)
}

13
go.mod Normal file
View File

@ -0,0 +1,13 @@
module git.openprivacy.ca/openprivacy/libricochet-go
require (
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca
github.com/golang/protobuf v1.2.0
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
)
go 1.13

21
go.sum Normal file
View File

@ -0,0 +1,21 @@
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g=
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw=
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b h1:Ib/yptP38nXZFMwqWSip+OKuMP9OkyDe3p+DssP8n9w=
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

86
identity/identity.go Normal file
View File

@ -0,0 +1,86 @@
package identity
import (
"crypto"
"crypto/rsa"
"encoding/asn1"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
)
// Identity is an encapsulation of Name, PrivateKey and other features
// that make up a Ricochet client.
// The purpose of Identity is to prevent other classes directly accessing private key
// and to ensure the integrity of security-critical functions.
type Identity struct {
Name string
pk *rsa.PrivateKey
edpk *ed25519.PrivateKey
edpubk *ed25519.PublicKey
}
// Init loads an identity from a file. Currently file should be a private_key
// but this may change in the future. //XXX
func Init(filename string) Identity {
pk, err := utils.LoadPrivateKeyFromFile(filename)
if err == nil {
return Identity{"", pk, nil, nil}
}
return Identity{}
}
// Initialize is a courtesy function for initializing an Identity in-code.
func Initialize(name string, pk *rsa.PrivateKey) Identity {
return Identity{name, pk, nil, nil}
}
// InitializeV3 is a courtesy function for initializing a V3 Identity in-code.
func InitializeV3(name string, pk *ed25519.PrivateKey, pubk *ed25519.PublicKey) Identity {
return Identity{name, nil, pk, pubk}
}
// Initialized ensures that an Identity has been assigned a private_key and
// is ready to perform operations.
func (i *Identity) Initialized() bool {
if i.pk == nil {
if i.edpk == nil {
return false
}
}
return true
}
// PublicKeyBytes returns the public key associated with this Identity in serializable-friendly
// format.
func (i *Identity) PublicKeyBytes() []byte {
if i.edpk != nil {
return *i.edpubk
}
publicKeyBytes, _ := asn1.Marshal(rsa.PublicKey{
N: i.pk.PublicKey.N,
E: i.pk.PublicKey.E,
})
return publicKeyBytes
}
// EDH performs a diffie helman operation on this identities private key with the given public key.
func (i *Identity) EDH(key ed25519.PublicKey) []byte {
secret := utils.EDH(*i.edpk, key)
return secret[:]
}
// Hostname provides the onion address associated with this Identity.
func (i *Identity) Hostname() string {
if i.pk != nil {
return utils.GetTorHostname(i.PublicKeyBytes())
}
return utils.GetTorV3Hostname(*i.edpubk)
}
// Sign produces a cryptographic signature using this Identities private key.
func (i *Identity) Sign(challenge []byte) ([]byte, error) {
return rsa.SignPKCS1v15(nil, i.pk, crypto.SHA256, challenge)
}

34
identity/identity_test.go Normal file
View File

@ -0,0 +1,34 @@
package identity
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
"testing"
)
func TestIdentity(t *testing.T) {
id := Identity{}
if id.Initialized() != false {
t.Errorf("Identity should not be initialized")
}
id = Init("../testing/private_key")
if id.Initialized() == false {
t.Errorf("Identity should be initialized")
}
if id.Hostname() != "kwke2hntvyfqm7dr" {
t.Errorf("Expected %v as Hostname() got: %v", "kwke2hntvyfqm7dr", id.Hostname())
}
mac := hmac.New(sha256.New, []byte("Hello"))
mac.Write([]byte("World"))
hmac := mac.Sum(nil)
bytes, err := id.Sign(hmac)
expected := "b0a0a0562735b559e0efb5b3431f1aa31ddc90d2cff114d0dc05980351a4ddc6086d92efdded8a7c447a2bab4afc5f031755738d1b21edba72680dea0e33b62e914faa1f596d5f76ca0ee91cb06e4ebab748a222cc860437b7c7afd12ebee6d6998b52183bd9eb9d5b96ea95900245480539464fa889719925e569cac0cecbc1"
actual := fmt.Sprintf("%x", bytes)
if expected != actual || err != nil {
t.Errorf("Identity sign failed, %v %v", actual, err)
}
}

View File

@ -0,0 +1,163 @@
package goricochet
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"io"
"net"
"testing"
)
func TestNegotiateInboundVersions(t *testing.T) {
connect := func() {
conn, err := net.Dial("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
conn.Write([]byte{0x49, 0x4D, 0x01, 0x03})
}
l, err := net.Listen("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer l.Close()
go connect()
conn, err := l.Accept()
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
_, err = NegotiateVersionInbound(conn)
if err != nil {
t.Errorf("Expected Success Got %v", err)
}
}
func TestBadProtcolLength(t *testing.T) {
connect := func() {
conn, err := net.Dial("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
conn.Write([]byte{0x49, 0x4D})
}
l, err := net.Listen("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer l.Close()
go connect()
conn, err := l.Accept()
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
_, err = NegotiateVersionInbound(conn)
if err != io.ErrUnexpectedEOF {
t.Errorf("Invalid Error Received. Expected ErrUnexpectedEOF. Got %v", err)
}
}
func TestNoSupportedVersions(t *testing.T) {
connect := func() {
conn, err := net.Dial("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
conn.Write([]byte{0x49, 0x4D, 0x00, 0xFF})
}
l, err := net.Listen("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer l.Close()
go connect()
conn, err := l.Accept()
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
_, err = NegotiateVersionInbound(conn)
if err != utils.VersionNegotiationError {
t.Errorf("Invalid Error Received. Expected VersionNegotiationError. Got %v", err)
}
}
func TestInvalidVersionList(t *testing.T) {
connect := func() {
conn, err := net.Dial("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
conn.Write([]byte{0x49, 0x4D, 0x01})
}
l, err := net.Listen("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer l.Close()
go connect()
conn, err := l.Accept()
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
_, err = NegotiateVersionInbound(conn)
if err != utils.VersionNegotiationError {
t.Errorf("Invalid Error Received. Expected VersionNegotiationError. Got %v", err)
}
}
func TestNoCompatibleVersions(t *testing.T) {
connect := func() {
conn, err := net.Dial("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
conn.Write([]byte{0x49, 0x4D, 0x01, 0xFF})
}
l, err := net.Listen("tcp", ":4000")
if err != nil {
t.Fatal(err)
}
defer l.Close()
go connect()
conn, err := l.Accept()
if err != nil {
t.Errorf("Error setting up test: %v", err)
}
_, err = NegotiateVersionInbound(conn)
if err != utils.VersionNegotiationFailed {
t.Errorf("Invalid Error Received. Expected VersionNegotiationFailed. Got %v", err)
}
}

219
log/log.go Normal file
View File

@ -0,0 +1,219 @@
package log
import (
"fmt"
golog "log"
"os"
"runtime"
"strings"
)
// Level indicates the level a log should be classified at
type Level int
// The possible levels a log can have
const (
LevelDebug Level = iota + 1
LevelInfo
LevelWarn
LevelError
)
var levelString = map[Level]string{LevelDebug: "[DBUG]", LevelInfo: "[INFO]", LevelWarn: "[WARN]", LevelError: "[ERR ]"}
// Logger is a go Logger wrapper that adds log levels and pattern filtering
// It allows high level 'log level' filtering broadly
// It allows allows two addition levels of filtering:
// everythingFrom which allows inclusion of packages/patterns along with general logging
// nothingExcept which allows focusing on just listed areas
type Logger struct {
logger *golog.Logger
level Level
nothingExceptPatterns []string
everythingFromPatterns []string
excludeFromPatterns []string
}
// New returns a new Logger with a filter set to the supplied level
func New(level Level) *Logger {
return &Logger{logger: golog.New(os.Stderr, "", golog.Ldate|golog.Ltime), level: level, everythingFromPatterns: make([]string, 0), nothingExceptPatterns: make([]string, 0)}
}
// NewFile returns a new Logger that logs to the supplied file with a filter set to the supplied level
func NewFile(level Level, filename string) (*Logger, error) {
logfile, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return nil, err
}
return &Logger{logger: golog.New(logfile, "", golog.Ldate|golog.Ltime), level: level, everythingFromPatterns: make([]string, 0), nothingExceptPatterns: make([]string, 0)}, nil
}
var std = New(LevelWarn)
// SetStd sets the default logger all other functions use
func SetStd(logger *Logger) {
std = logger
}
// filter
func (l *Logger) filter(level Level) bool {
_, file, _, ok := runtime.Caller(3)
if !ok {
file = "???"
}
for _, pattern := range l.excludeFromPatterns {
if strings.Contains(file, pattern) {
return false
}
}
for _, pattern := range l.everythingFromPatterns {
if strings.Contains(file, pattern) {
return true
}
}
for _, pattern := range l.nothingExceptPatterns {
if strings.Contains(file, pattern) {
return true
}
}
if len(l.nothingExceptPatterns) > 0 {
return false
}
return l.aboveLevel(level)
}
func (l *Logger) aboveLevel(level Level) bool {
return level >= l.level
}
// SetLevel adjusts the output filter by logging level
func (l *Logger) SetLevel(level Level) {
l.level = level
}
// AddNothingExceptFilter enables strong filtering showing logs only for things on the approved list, adding this pattern to the list
func (l *Logger) AddNothingExceptFilter(pattern string) {
l.nothingExceptPatterns = append(l.nothingExceptPatterns, pattern)
}
// AddEverythingFromPattern adds a pattern to skip log level filtering, guaranteeing all logs matching the pattern are seen
func (l *Logger) AddEverythingFromPattern(pattern string) {
l.everythingFromPatterns = append(l.everythingFromPatterns, pattern)
}
// ExcludeFromPattern adds a pattern to exclude logs from
func (l *Logger) ExcludeFromPattern(pattern string) {
l.excludeFromPatterns = append(l.excludeFromPatterns, pattern)
}
func (l *Logger) header(level Level) string {
_, file, _, ok := runtime.Caller(3)
if !ok {
file = "???"
} else {
short := file
count := 0
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
count++
if count == 2 {
break
}
}
}
file = short
}
return file + " " + levelString[level] + " "
}
// Printf outputs the format and variables, assuming it passes the filter levels
func (l *Logger) Printf(level Level, format string, v ...interface{}) {
if l.filter(level) {
l.logger.Output(3, l.header(level)+fmt.Sprintf(format, v...))
}
}
// Println outputs the variables assuming the filter levels are passed
func (l *Logger) Println(level Level, v ...interface{}) {
if l.filter(level) {
l.logger.Output(3, l.header(level)+fmt.Sprintln(v...))
}
}
// SetLevel changes the filtering level of the system logger
func SetLevel(level Level) {
std.SetLevel(level)
}
// AddNothingExceptFilter enables strong filtering showing logs only for things on the approved list, adding this pattern to the list
func AddNothingExceptFilter(pattern string) {
std.AddNothingExceptFilter(pattern)
}
// AddEverythingFromPattern adds a pattern to skip log level filtering, guaranteeing all logs matching the pattern are seen
func AddEverythingFromPattern(pattern string) {
std.AddEverythingFromPattern(pattern)
}
// ExcludeFromPattern adds a pattern to exclude logs from
func ExcludeFromPattern(pattern string) {
std.ExcludeFromPattern(pattern)
}
// Printf outputs the format with variables assuming it passes the filter level
func Printf(level Level, format string, v ...interface{}) {
std.Printf(level, format, v...)
}
// Println outputs the varibles assuming they pass the filter level
func Println(level Level, v ...interface{}) {
std.Println(level, v...)
}
// Debugf outputs the format and variables at the Debug level
func Debugf(format string, v ...interface{}) {
std.Printf(LevelDebug, format, v...)
}
// Infof outputs the format and variables at the Info level
func Infof(format string, v ...interface{}) {
std.Printf(LevelInfo, format, v...)
}
// Warnf outputs the format and variables at the Warning level
func Warnf(format string, v ...interface{}) {
std.Printf(LevelWarn, format, v...)
}
// Errorf outputs the format and variables at the Error level
func Errorf(format string, v ...interface{}) {
std.Printf(LevelError, format, v...)
}
// Debugln outputs the variables at the Debug level
func Debugln(v ...interface{}) {
std.Println(LevelDebug, v...)
}
// Infoln outputs the variables at the Info level
func Infoln(v ...interface{}) {
std.Println(LevelInfo, v...)
}
// Warnln outputs the variables at the Warn level
func Warnln(v ...interface{}) {
std.Println(LevelWarn, v...)
}
// Errorln outputs the variables at the Error level
func Errorln(v ...interface{}) {
std.Println(LevelError, v...)
}

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,39 +0,0 @@
package goricochet
import "testing"
func TestOpenChatChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
_, err := messageBuilder.OpenChannel(1, "im.ricochet.chat")
if err != nil {
t.Errorf("Error building open chat channel message: %s", err)
}
// TODO: More Indepth Test Of Output
}
func TestOpenContactRequestChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
_, err := messageBuilder.OpenContactRequestChannel(3, "Nickname", "Message")
if err != nil {
t.Errorf("Error building open contact request channel message: %s", err)
}
// TODO: More Indepth Test Of Output
}
func TestOpenAuthenticationChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
_, err := messageBuilder.OpenAuthenticationChannel(1, [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
if err != nil {
t.Errorf("Error building open authentication channel message: %s", err)
}
// TODO: More Indepth Test Of Output
}
func TestChatMessage(t *testing.T) {
messageBuilder := new(MessageBuilder)
_, err := messageBuilder.ChatMessage("Hello World", 0)
if err != nil {
t.Errorf("Error building chat message: %s", err)
}
// TODO: More Indepth Test Of Output
}

View File

@ -1,300 +0,0 @@
package goricochet
import (
"crypto"
"crypto/rsa"
"encoding/asn1"
"github.com/s-rah/go-ricochet/utils"
"net"
)
// OpenConnection encapsulates the state required to maintain a connection to
// a ricochet service.
// Notably OpenConnection does not enforce limits on the channelIDs, channel Assignments
// or the direction of messages. These are considered to be service enforced rules.
// (and services are considered to be the best to define them).
type OpenConnection struct {
conn net.Conn
authHandler map[int32]*AuthenticationHandler
channels map[int32]string
rni utils.RicochetNetworkInterface
Client bool
IsAuthed bool
MyHostname string
OtherHostname string
Closed bool
}
// Init initializes a OpenConnection object to a default state.
func (oc *OpenConnection) Init(outbound bool, conn net.Conn) {
oc.conn = conn
oc.authHandler = make(map[int32]*AuthenticationHandler)
oc.channels = make(map[int32]string)
oc.rni = new(utils.RicochetNetwork)
oc.Client = outbound
oc.IsAuthed = false
oc.MyHostname = ""
oc.OtherHostname = ""
}
// UnsetChannel removes a type association from the channel.
func (oc *OpenConnection) UnsetChannel(channel int32) {
oc.channels[channel] = "none"
}
// GetChannelType returns the type of the channel on this connection
func (oc *OpenConnection) GetChannelType(channel int32) string {
if val, ok := oc.channels[channel]; ok {
return val
}
return "none"
}
func (oc *OpenConnection) setChannel(channel int32, channelType string) {
oc.channels[channel] = channelType
}
// HasChannel returns true if the connection has a channel of an associated type, false otherwise
func (oc *OpenConnection) HasChannel(channelType string) bool {
for _, val := range oc.channels {
if val == channelType {
return true
}
}
return false
}
// CloseChannel closes a given channel
// Prerequisites:
// * Must have previously connected to a service
func (oc *OpenConnection) CloseChannel(channel int32) {
oc.UnsetChannel(channel)
oc.rni.SendRicochetPacket(oc.conn, channel, []byte{})
}
// Close closes the entire connection
func (oc *OpenConnection) Close() {
oc.conn.Close()
oc.Closed = true
}
// Authenticate opens an Authentication Channel and send a client cookie
// Prerequisites:
// * Must have previously connected to a service
func (oc *OpenConnection) Authenticate(channel int32) {
defer utils.RecoverFromError()
oc.authHandler[channel] = new(AuthenticationHandler)
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.OpenAuthenticationChannel(channel, oc.authHandler[channel].GenClientCookie())
utils.CheckError(err)
oc.setChannel(channel, "im.ricochet.auth.hidden-service")
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// ConfirmAuthChannel responds to a new authentication request.
// Prerequisites:
// * Must have previously connected to a service
func (oc *OpenConnection) ConfirmAuthChannel(channel int32, clientCookie [16]byte) {
defer utils.RecoverFromError()
oc.authHandler[channel] = new(AuthenticationHandler)
oc.authHandler[channel].AddClientCookie(clientCookie[:])
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.ConfirmAuthChannel(channel, oc.authHandler[channel].GenServerCookie())
utils.CheckError(err)
oc.setChannel(channel, "im.ricochet.auth.hidden-service")
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// SendProof sends an authentication proof in response to a challenge.
// Prerequisites:
// * Must have previously connected to a service
// * channel must be of type auth
func (oc *OpenConnection) SendProof(channel int32, serverCookie [16]byte, publicKeyBytes []byte, privateKey *rsa.PrivateKey) {
if oc.authHandler[channel] == nil {
return // NoOp
}
oc.authHandler[channel].AddServerCookie(serverCookie[:])
challenge := oc.authHandler[channel].GenChallenge(oc.MyHostname, oc.OtherHostname)
signature, _ := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, challenge)
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.Proof(publicKeyBytes, signature)
utils.CheckError(err)
oc.rni.SendRicochetPacket(oc.conn, channel, data)
}
// ValidateProof determines if the given public key and signature align with the
// already established challenge vector for this communication
// Prerequisites:
// * Must have previously connected to a service
// * Client and Server must have already sent their respective cookies (Authenticate and ConfirmAuthChannel)
func (oc *OpenConnection) ValidateProof(channel int32, publicKeyBytes []byte, signature []byte) bool {
if oc.authHandler[channel] == nil {
return false
}
provisionalHostname := utils.GetTorHostname(publicKeyBytes)
publicKey := new(rsa.PublicKey)
_, err := asn1.Unmarshal(publicKeyBytes, publicKey)
if err != nil {
return false
}
challenge := oc.authHandler[channel].GenChallenge(provisionalHostname, oc.MyHostname)
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, challenge[:], signature)
if err == nil {
oc.OtherHostname = provisionalHostname
return true
}
return false
}
// SendAuthenticationResult responds to an existed authentication Proof
// Prerequisites:
// * Must have previously connected to a service
// * channel must be of type auth
func (oc *OpenConnection) SendAuthenticationResult(channel int32, accepted bool, isKnownContact bool) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.AuthResult(accepted, isKnownContact)
utils.CheckError(err)
oc.rni.SendRicochetPacket(oc.conn, channel, data)
}
// OpenChatChannel opens a new chat channel with the given id
// Prerequisites:
// * Must have previously connected to a service
// * If acting as the client, id must be odd, else even
func (oc *OpenConnection) OpenChatChannel(channel int32) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.OpenChannel(channel, "im.ricochet.chat")
utils.CheckError(err)
oc.setChannel(channel, "im.ricochet.chat")
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// OpenChannel opens a new chat channel with the given id
// Prerequisites:
// * Must have previously connected to a service
// * If acting as the client, id must be odd, else even
func (oc *OpenConnection) OpenChannel(channel int32, channelType string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.OpenChannel(channel, channelType)
utils.CheckError(err)
oc.setChannel(channel, channelType)
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// AckOpenChannel acknowledges a previously received open channel message
// Prerequisites:
// * Must have previously connected and authenticated to a service
func (oc *OpenConnection) AckOpenChannel(channel int32, channeltype string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.AckOpenChannel(channel)
utils.CheckError(err)
oc.setChannel(channel, channeltype)
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// RejectOpenChannel acknowledges a rejects a previously received open channel message
// Prerequisites:
// * Must have previously connected
func (oc *OpenConnection) RejectOpenChannel(channel int32, errortype string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.RejectOpenChannel(channel, errortype)
utils.CheckError(err)
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// SendContactRequest initiates a contact request to the server.
// Prerequisites:
// * Must have previously connected and authenticated to a service
func (oc *OpenConnection) SendContactRequest(channel int32, nick string, message string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.OpenContactRequestChannel(channel, nick, message)
utils.CheckError(err)
oc.setChannel(channel, "im.ricochet.contact.request")
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// AckContactRequestOnResponse responds a contact request from a client
// Prerequisites:
// * Must have previously connected and authenticated to a service
// * Must have previously received a Contact Request
func (oc *OpenConnection) AckContactRequestOnResponse(channel int32, status string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.ReplyToContactRequestOnResponse(channel, status)
utils.CheckError(err)
oc.setChannel(channel, "im.ricochet.contact.request")
oc.rni.SendRicochetPacket(oc.conn, 0, data)
}
// AckContactRequest responds to contact request from a client
// Prerequisites:
// * Must have previously connected and authenticated to a service
// * Must have previously received a Contact Request
func (oc *OpenConnection) AckContactRequest(channel int32, status string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.ReplyToContactRequest(channel, status)
utils.CheckError(err)
oc.setChannel(channel, "im.ricochet.contact.request")
oc.rni.SendRicochetPacket(oc.conn, channel, data)
}
// AckChatMessage acknowledges a previously received chat message.
// Prerequisites:
// * Must have previously connected and authenticated to a service
// * Must have established a known contact status with the other service
// * Must have received a Chat message on an open im.ricochet.chat channel with the messageID
func (oc *OpenConnection) AckChatMessage(channel int32, messageID int32) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.AckChatMessage(messageID)
utils.CheckError(err)
oc.rni.SendRicochetPacket(oc.conn, channel, data)
}
// SendMessage sends a Chat Message (message) to a give Channel (channel).
// Prerequisites:
// * Must have previously connected and authenticated to a service
// * Must have established a known contact status with the other service
// * Must have previously opened channel with OpenChanel of type im.ricochet.chat
func (oc *OpenConnection) SendMessage(channel int32, message string) {
defer utils.RecoverFromError()
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.ChatMessage(message, 0)
utils.CheckError(err)
oc.rni.SendRicochetPacket(oc.conn, channel, data)
}

View File

@ -1,7 +0,0 @@
package goricochet
import "testing"
func TestOpenConnectionAuth(t *testing.T) {
}

View File

@ -0,0 +1,75 @@
package goricochet
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"net"
"testing"
"time"
)
func TestOutboundVersionNegotiation(t *testing.T) {
go func() {
ln, _ := net.Listen("tcp", "127.0.0.1:12001")
conn, _ := ln.Accept()
b := make([]byte, 4)
n, err := conn.Read(b)
if n == 4 && err == nil {
conn.Write([]byte{0x03})
}
conn.Close()
}()
time.Sleep(time.Second * 1)
conn, err := net.Dial("tcp", ":12001")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = NegotiateVersionOutbound(conn, "")
if err != nil {
t.Errorf("Expected success got %v", err)
}
}
func TestInvalidServer(t *testing.T) {
go func() {
ln, _ := net.Listen("tcp", "127.0.0.1:12002")
conn, _ := ln.Accept()
b := make([]byte, 4)
conn.Read(b)
conn.Write([]byte{})
conn.Close()
}()
time.Sleep(time.Second * 1)
conn, err := net.Dial("tcp", ":12002")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = NegotiateVersionOutbound(conn, "")
if err != utils.VersionNegotiationError {
t.Errorf("Expected VersionNegotiationError got %v", err)
}
}
func TestInvalidResponse(t *testing.T) {
go func() {
ln, _ := net.Listen("tcp", "127.0.0.1:12003")
conn, _ := ln.Accept()
b := make([]byte, 4)
n, err := conn.Read(b)
if n == 4 && err == nil {
conn.Write([]byte{0xFF})
}
conn.Close()
}()
time.Sleep(time.Second * 1)
conn, err := net.Dial("tcp", ":12003")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = NegotiateVersionOutbound(conn, "")
if err != utils.VersionNegotiationFailed {
t.Errorf("Expected VersionNegotiationFailed got %v", err)
}
}

32
policies/timeoutpolicy.go Normal file
View File

@ -0,0 +1,32 @@
package policies
import (
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"time"
)
// TimeoutPolicy is a convieance interface for enforcing common timeout patterns
type TimeoutPolicy time.Duration
// Selection of common timeout policies
const (
UnknownPurposeTimeout TimeoutPolicy = TimeoutPolicy(15 * time.Second)
)
// ExecuteAction runs a function and returns an error if it hasn't returned
// by the time specified by TimeoutPolicy
func (tp *TimeoutPolicy) ExecuteAction(action func() error) error {
c := make(chan error)
go func() {
c <- action()
}()
tick := time.Tick(time.Duration(*tp))
select {
case <-tick:
return utils.ActionTimedOutError
case err := <-c:
return err
}
}

View File

@ -0,0 +1,30 @@
package policies
import (
"testing"
"time"
)
func TestTimeoutPolicy(t *testing.T) {
policy := UnknownPurposeTimeout
result := func() error {
time.Sleep(2 * time.Second)
return nil
}
err := policy.ExecuteAction(result)
if err != nil {
t.Errorf("Action should have returned nil: %v", err)
}
}
func TestTimeoutPolicyExpires(t *testing.T) {
policy := TimeoutPolicy(1 * time.Second)
result := func() error {
time.Sleep(5 * time.Second)
return nil
}
err := policy.ExecuteAction(result)
if err == nil {
t.Errorf("Action should have returned err")
}
}

View File

@ -1,396 +1,90 @@
package goricochet
import (
"errors"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/auth"
"github.com/s-rah/go-ricochet/chat"
"github.com/s-rah/go-ricochet/contact"
"github.com/s-rah/go-ricochet/control"
"github.com/s-rah/go-ricochet/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"io"
"log"
"net"
"strconv"
)
// Ricochet is a protocol to conducting anonymous IM.
type Ricochet struct {
newconns chan *OpenConnection
networkResolver utils.NetworkResolver
rni utils.RicochetNetworkInterface
}
// Init sets up the Ricochet object.
func (r *Ricochet) Init() {
r.newconns = make(chan *OpenConnection)
r.networkResolver = utils.NetworkResolver{}
r.rni = new(utils.RicochetNetwork)
}
// Connect sets up a client ricochet connection to host e.g. qn6uo4cmsrfv4kzq.onion. If this
// function finished successfully then the connection can be assumed to
// be open and authenticated.
// To specify a local port using the format "127.0.0.1:[port]|ricochet-id".
func (r *Ricochet) Connect(host string) (*OpenConnection, error) {
var err error
conn, host, err := r.networkResolver.Resolve(host)
// Open establishes a protocol session on an established net.conn, and returns a new
// OpenConnection instance representing this connection. On error, the connection
// will be closed. This function blocks until version negotiation has completed.
// The application should call Process() on the returned OpenConnection to continue
// handling protocol messages.
func Open(acn connectivity.ACN, remoteHostname string) (*connection.Connection, error) {
conn, remoteHostname, err := acn.Open(remoteHostname)
if err != nil {
return nil, err
}
return r.ConnectOpen(conn, host)
}
// ConnectOpen attempts to open up a new connection to the given host. Returns a
// pointer to the OpenConnection or an error.
func (r *Ricochet) ConnectOpen(conn net.Conn, host string) (*OpenConnection, error) {
oc, err := r.negotiateVersion(conn, true)
rc, err := NegotiateVersionOutbound(conn, remoteHostname)
if err != nil {
conn.Close()
return nil, err
}
oc.OtherHostname = host
r.newconns <- oc
return oc, nil
return rc, nil
}
// Server launches a new server listening on port
func (r *Ricochet) Server(service RicochetService, port int) {
ln, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port))
if err != nil {
log.Printf("Cannot Listen on Port %v", port)
return
// NegotiateVersionOutbound takes an open network connection and executes
// the ricochet version negotiation procedure.
func NegotiateVersionOutbound(conn net.Conn, remoteHostname string) (*connection.Connection, error) {
versions := []byte{0x49, 0x4D, 0x01, 0x03}
if n, err := conn.Write(versions); err != nil || n < len(versions) {
return nil, utils.VersionNegotiationError
}
r.ServeListener(service, ln)
}
// ServeListener processes all messages given by the listener ln with the given
// RicochetService, service.
func (r *Ricochet) ServeListener(service RicochetService, ln net.Listener) {
go r.ProcessMessages(service)
service.OnReady()
for {
// accept connection on port
conn, err := ln.Accept()
if err != nil {
return
}
go r.processNewConnection(conn, service)
res := make([]byte, 1)
if _, err := io.ReadAtLeast(conn, res, len(res)); err != nil {
return nil, utils.VersionNegotiationError
}
}
// processNewConnection sets up a new connection
func (r *Ricochet) processNewConnection(conn net.Conn, service RicochetService) {
oc, err := r.negotiateVersion(conn, false)
if err == nil {
r.newconns <- oc
service.OnConnect(oc)
if res[0] != 0x03 {
return nil, utils.VersionNegotiationFailed
}
rc := connection.NewOutboundConnection(conn, remoteHostname)
return rc, nil
}
// ProcessMessages is intended to be a background thread listening for all messages
// a client will send. The given RicochetService will be used to respond to messages.
// Prerequisites:
// * Must have previously issued a successful Connect()
func (r *Ricochet) ProcessMessages(service RicochetService) {
for {
oc := <-r.newconns
if oc == nil {
return
}
go r.processConnection(oc, service)
// NegotiateVersionInbound takes in a connection and performs version negotiation
// as if that connection was a client. Returns a ricochet connection if successful
// error otherwise.
func NegotiateVersionInbound(conn net.Conn) (*connection.Connection, error) {
versions := []byte{0x49, 0x4D, 0x01, 0x03}
// Read version response header
header := make([]byte, 3)
if _, err := io.ReadAtLeast(conn, header, len(header)); err != nil {
return nil, err
}
}
// RequestStopMessageLoop requests that the ProcessMessages loop is stopped after handling all currently
// queued new connections.
func (r *Ricochet) RequestStopMessageLoop() {
r.newconns <- nil
}
// ProcessConnection starts a blocking process loop which continually waits for
// new messages to arrive from the connection and uses the given RicochetService
// to process them.
func (r *Ricochet) processConnection(oc *OpenConnection, service RicochetService) {
service.OnConnect(oc)
defer service.OnDisconnect(oc)
for {
if oc.Closed {
return
}
packet, err := r.rni.RecvRicochetPacket(oc.conn)
if err != nil {
oc.Close()
return
}
if len(packet.Data) == 0 {
service.OnChannelClosed(oc, packet.Channel)
continue
}
if packet.Channel == 0 {
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(packet.Data[:], res)
if err != nil {
service.OnGenericError(oc, packet.Channel)
continue
}
if res.GetOpenChannel() != nil {
opm := res.GetOpenChannel()
if oc.GetChannelType(opm.GetChannelIdentifier()) != "none" {
// Channel is already in use.
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
continue
}
// If I am a Client, the server can only open even numbered channels
if oc.Client && opm.GetChannelIdentifier()%2 != 0 {
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
continue
}
// If I am a Server, the client can only open odd numbered channels
if !oc.Client && opm.GetChannelIdentifier()%2 != 1 {
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
continue
}
switch opm.GetChannelType() {
case "im.ricochet.auth.hidden-service":
if oc.Client {
// Servers are authed by default and can't auth with hidden-service
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
} else if oc.IsAuthed {
// Can't auth if already authed
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
} else if oc.HasChannel("im.ricochet.auth.hidden-service") {
// Can't open more than 1 auth channel
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
} else {
clientCookie, err := proto.GetExtension(opm, Protocol_Data_AuthHiddenService.E_ClientCookie)
if err == nil {
clientCookieB := [16]byte{}
copy(clientCookieB[:], clientCookie.([]byte)[:])
service.OnAuthenticationRequest(oc, opm.GetChannelIdentifier(), clientCookieB)
} else {
// Must include Client Cookie
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
}
}
case "im.ricochet.chat":
if !oc.IsAuthed {
// Can't open chat channel if not authorized
service.OnUnauthorizedError(oc, opm.GetChannelIdentifier())
} else if !service.IsKnownContact(oc.OtherHostname) {
// Can't open chat channel if not a known contact
service.OnUnauthorizedError(oc, opm.GetChannelIdentifier())
} else {
service.OnOpenChannelRequest(oc, opm.GetChannelIdentifier(), "im.ricochet.chat")
}
case "im.ricochet.contact.request":
if oc.Client {
// Servers are not allowed to send contact requests
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
} else if !oc.IsAuthed {
// Can't open a contact channel if not authed
service.OnUnauthorizedError(oc, opm.GetChannelIdentifier())
} else if oc.HasChannel("im.ricochet.contact.request") {
// Only 1 contact channel is allowed to be open at a time
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
} else {
contactRequestI, err := proto.GetExtension(opm, Protocol_Data_ContactRequest.E_ContactRequest)
if err == nil {
contactRequest, check := contactRequestI.(*Protocol_Data_ContactRequest.ContactRequest)
if check {
service.OnContactRequest(oc, opm.GetChannelIdentifier(), contactRequest.GetNickname(), contactRequest.GetMessageText())
break
}
}
service.OnBadUsageError(oc, opm.GetChannelIdentifier())
}
default:
service.OnUnknownTypeError(oc, opm.GetChannelIdentifier())
}
} else if res.GetChannelResult() != nil {
crm := res.GetChannelResult()
if crm.GetOpened() {
switch oc.GetChannelType(crm.GetChannelIdentifier()) {
case "im.ricochet.auth.hidden-service":
serverCookie, err := proto.GetExtension(crm, Protocol_Data_AuthHiddenService.E_ServerCookie)
if err == nil {
serverCookieB := [16]byte{}
copy(serverCookieB[:], serverCookie.([]byte)[:])
service.OnAuthenticationChallenge(oc, crm.GetChannelIdentifier(), serverCookieB)
} else {
service.OnBadUsageError(oc, crm.GetChannelIdentifier())
}
case "im.ricochet.chat":
service.OnOpenChannelRequestSuccess(oc, crm.GetChannelIdentifier())
case "im.ricochet.contact.request":
responseI, err := proto.GetExtension(res.GetChannelResult(), Protocol_Data_ContactRequest.E_Response)
if err == nil {
response, check := responseI.(*Protocol_Data_ContactRequest.Response)
if check {
service.OnContactRequestAck(oc, crm.GetChannelIdentifier(), response.GetStatus().String())
break
}
}
service.OnBadUsageError(oc, crm.GetChannelIdentifier())
default:
service.OnBadUsageError(oc, crm.GetChannelIdentifier())
}
} else {
if oc.GetChannelType(crm.GetChannelIdentifier()) != "none" {
service.OnFailedChannelOpen(oc, crm.GetChannelIdentifier(), crm.GetCommonError().String())
} else {
oc.CloseChannel(crm.GetChannelIdentifier())
}
}
} else {
// Unknown Message
oc.CloseChannel(packet.Channel)
}
} else if oc.GetChannelType(packet.Channel) == "im.ricochet.auth.hidden-service" {
res := new(Protocol_Data_AuthHiddenService.Packet)
err := proto.Unmarshal(packet.Data[:], res)
if err != nil {
oc.CloseChannel(packet.Channel)
continue
}
if res.GetProof() != nil && !oc.Client { // Only Clients Send Proofs
service.OnAuthenticationProof(oc, packet.Channel, res.GetProof().GetPublicKey(), res.GetProof().GetSignature(), service.IsKnownContact(oc.OtherHostname))
} else if res.GetResult() != nil && oc.Client { // Only Servers Send Results
service.OnAuthenticationResult(oc, packet.Channel, res.GetResult().GetAccepted(), res.GetResult().GetIsKnownContact())
} else {
// If neither of the above are satisfied we just close the connection
oc.Close()
}
} else if oc.GetChannelType(packet.Channel) == "im.ricochet.chat" {
// NOTE: These auth checks should be redundant, however they
// are included here for defense-in-depth if for some reason
// a previously authed connection becomes untrusted / not known and
// the state is not cleaned up.
if !oc.IsAuthed {
// Can't send chat messages if not authorized
service.OnUnauthorizedError(oc, packet.Channel)
} else if !service.IsKnownContact(oc.OtherHostname) {
// Can't send chat message if not a known contact
service.OnUnauthorizedError(oc, packet.Channel)
} else {
res := new(Protocol_Data_Chat.Packet)
err := proto.Unmarshal(packet.Data[:], res)
if err != nil {
oc.CloseChannel(packet.Channel)
continue
}
if res.GetChatMessage() != nil {
service.OnChatMessage(oc, packet.Channel, int32(res.GetChatMessage().GetMessageId()), res.GetChatMessage().GetMessageText())
} else if res.GetChatAcknowledge() != nil {
service.OnChatMessageAck(oc, packet.Channel, int32(res.GetChatMessage().GetMessageId()))
} else {
// If neither of the above are satisfied we just close the connection
oc.Close()
}
}
} else if oc.GetChannelType(packet.Channel) == "im.ricochet.contact.request" {
// NOTE: These auth checks should be redundant, however they
// are included here for defense-in-depth if for some reason
// a previously authed connection becomes untrusted / not known and
// the state is not cleaned up.
if !oc.Client {
// Clients are not allowed to send contact request responses
service.OnBadUsageError(oc, packet.Channel)
} else if !oc.IsAuthed {
// Can't send a contact request if not authed
service.OnBadUsageError(oc, packet.Channel)
} else {
res := new(Protocol_Data_ContactRequest.Response)
err := proto.Unmarshal(packet.Data[:], res)
log.Printf("%v", res)
if err != nil {
oc.CloseChannel(packet.Channel)
continue
}
service.OnContactRequestAck(oc, packet.Channel, res.GetStatus().String())
}
} else if oc.GetChannelType(packet.Channel) == "none" {
// Invalid Channel Assignment
oc.CloseChannel(packet.Channel)
} else {
oc.Close()
}
if header[0] != versions[0] || header[1] != versions[1] || header[2] < 1 {
return nil, utils.VersionNegotiationError
}
}
// Perform version negotiation on the connection, and create an OpenConnection if successful
func (r *Ricochet) negotiateVersion(conn net.Conn, outbound bool) (*OpenConnection, error) {
versions := []byte{0x49, 0x4D, 0x01, 0x01}
// Read list of supported versions (which is header[2] bytes long)
versionList := make([]byte, header[2])
if _, err := io.ReadAtLeast(conn, versionList, len(versionList)); err != nil {
return nil, utils.VersionNegotiationError
}
// Outbound side of the connection sends a list of supported versions
if outbound {
if n, err := conn.Write(versions); err != nil || n < len(versions) {
return nil, err
}
res := make([]byte, 1)
if _, err := io.ReadAtLeast(conn, res, len(res)); err != nil {
return nil, err
}
if res[0] != 0x01 {
return nil, errors.New("unsupported protocol version")
}
} else {
// Read version response header
header := make([]byte, 3)
if _, err := io.ReadAtLeast(conn, header, len(header)); err != nil {
return nil, err
}
if header[0] != versions[0] || header[1] != versions[1] || header[2] < 1 {
return nil, errors.New("invalid protocol response")
}
// Read list of supported versions (which is header[2] bytes long)
versionList := make([]byte, header[2])
if _, err := io.ReadAtLeast(conn, versionList, len(versionList)); err != nil {
return nil, err
}
selectedVersion := byte(0xff)
for _, v := range versionList {
if v == 0x01 {
selectedVersion = v
break
}
}
if n, err := conn.Write([]byte{selectedVersion}); err != nil || n < 1 {
return nil, err
}
if selectedVersion == 0xff {
return nil, errors.New("no supported protocol version")
selectedVersion := byte(0xff)
for _, v := range versionList {
if v == 0x03 {
selectedVersion = v
break
}
}
oc := new(OpenConnection)
oc.Init(outbound, conn)
return oc, nil
if n, err := conn.Write([]byte{selectedVersion}); err != nil || n < 1 {
return nil, utils.VersionNegotiationFailed
}
if selectedVersion == 0xff {
return nil, utils.VersionNegotiationFailed
}
rc := connection.NewInboundConnection(conn)
return rc, nil
}

65
ricochet_test.go Normal file
View File

@ -0,0 +1,65 @@
package goricochet
import (
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"net"
"testing"
"time"
)
func SimpleServer() {
ln, _ := net.Listen("tcp", "127.0.0.1:11000")
conn, _ := ln.Accept()
b := make([]byte, 4)
n, err := conn.Read(b)
if n == 4 && err == nil {
conn.Write([]byte{0x03})
}
conn.Close()
}
func TestRicochetOpen(t *testing.T) {
acn := connectivity.LocalProvider()
go SimpleServer()
// Wait for Server to Initialize
time.Sleep(time.Second)
rc, err := Open(acn, "127.0.0.1:11000|abcdefghijklmno.onion")
if err == nil {
if rc.IsInbound {
t.Errorf("RicochetConnection declares itself as an Inbound connection after an Outbound attempt...that shouldn't happen")
}
return
}
t.Errorf("RicochetProtocol: Open Failed: %v", err)
}
func BadServer() {
ln, _ := net.Listen("tcp", "127.0.0.1:11001")
conn, _ := ln.Accept()
b := make([]byte, 4)
n, err := conn.Read(b)
if n == 4 && err == nil {
conn.Write([]byte{0xFF})
}
conn.Close()
}
func TestRicochetOpenWithError(t *testing.T) {
acn := connectivity.LocalProvider()
go BadServer()
// Wait for Server to Initialize
time.Sleep(time.Second)
_, err := Open(acn, "127.0.0.1:11001|abcdefghijklmno.onion")
if err == nil {
t.Errorf("Open should have failed because of bad version negotiation.")
}
}
func TestRicochetOpenWithNoServer(t *testing.T) {
acn := connectivity.LocalProvider()
_, err := Open(acn, "127.0.0.1:11002|abcdefghijklmno.onion")
if err == nil {
t.Errorf("Open should have failed because of bad version negotiation.")
}
}

View File

@ -1,36 +0,0 @@
package goricochet
// RicochetService provides an interface for building automated ricochet applications.
type RicochetService interface {
OnReady()
OnConnect(oc *OpenConnection)
OnDisconnect(oc *OpenConnection)
// Authentication Management
OnAuthenticationRequest(oc *OpenConnection, channelID int32, clientCookie [16]byte)
OnAuthenticationChallenge(oc *OpenConnection, channelID int32, serverCookie [16]byte)
OnAuthenticationProof(oc *OpenConnection, channelID int32, publicKey []byte, signature []byte, isKnownContact bool)
OnAuthenticationResult(oc *OpenConnection, channelID int32, result bool, isKnownContact bool)
// Contact Management
IsKnownContact(hostname string) bool
OnContactRequest(oc *OpenConnection, channelID int32, nick string, message string)
OnContactRequestAck(oc *OpenConnection, channelID int32, status string)
// Managing Channels
OnOpenChannelRequest(oc *OpenConnection, channelID int32, channelType string)
OnOpenChannelRequestSuccess(oc *OpenConnection, channelID int32)
OnChannelClosed(oc *OpenConnection, channelID int32)
// Chat Messages
OnChatMessage(oc *OpenConnection, channelID int32, messageID int32, message string)
OnChatMessageAck(oc *OpenConnection, channelID int32, messageID int32)
// Handle Errors
OnFailedChannelOpen(oc *OpenConnection, channelID int32, errorType string)
OnGenericError(oc *OpenConnection, channelID int32)
OnUnknownTypeError(oc *OpenConnection, channelID int32)
OnUnauthorizedError(oc *OpenConnection, channelID int32)
OnBadUsageError(oc *OpenConnection, channelID int32)
OnFailedError(oc *OpenConnection, channelID int32)
}

View File

@ -1,182 +0,0 @@
package goricochet
import (
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"errors"
"github.com/s-rah/go-ricochet/utils"
"io/ioutil"
"log"
)
// StandardRicochetService implements all the necessary flows to implement a
// minimal, protocol compliant Ricochet Service. It can be built on by other
// applications to produce automated riochet applications.
type StandardRicochetService struct {
ricochet *Ricochet
privateKey *rsa.PrivateKey
serverHostname string
}
// Init initializes a StandardRicochetService with the cryptographic key given
// by filename.
func (srs *StandardRicochetService) Init(filename string) error {
srs.ricochet = new(Ricochet)
srs.ricochet.Init()
pemData, err := ioutil.ReadFile(filename)
if err != nil {
return errors.New("Could not setup ricochet service: could not read private key")
}
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "RSA PRIVATE KEY" {
return errors.New("Could not setup ricochet service: no valid PEM data found")
}
srs.privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return errors.New("Could not setup ricochet service: could not parse private key")
}
publicKeyBytes, _ := asn1.Marshal(rsa.PublicKey{
N: srs.privateKey.PublicKey.N,
E: srs.privateKey.PublicKey.E,
})
srs.serverHostname = utils.GetTorHostname(publicKeyBytes)
log.Printf("Initialised ricochet service for %s", srs.serverHostname)
return nil
}
// OnReady is called once a Server has been established (by calling Listen)
func (srs *StandardRicochetService) OnReady() {
}
// Listen starts the ricochet service. Listen must be called before any other method (apart from Init)
func (srs *StandardRicochetService) Listen(service RicochetService, port int) {
srs.ricochet.Server(service, port)
}
// Connect can be called to initiate a new client connection to a server
func (srs *StandardRicochetService) Connect(hostname string) error {
log.Printf("Connecting to...%s", hostname)
oc, err := srs.ricochet.Connect(hostname)
if err != nil {
return errors.New("Could not connect to: " + hostname + " " + err.Error())
}
oc.MyHostname = srs.serverHostname
return nil
}
// OnConnect is called when a client or server successfully passes Version Negotiation.
func (srs *StandardRicochetService) OnConnect(oc *OpenConnection) {
if oc.Client {
log.Printf("Sucessefully Connected to %s", oc.OtherHostname)
oc.IsAuthed = true // Connections to Servers are Considered Authenticated by Default
oc.Authenticate(1)
} else {
oc.MyHostname = srs.serverHostname
}
}
// OnDisconnect is called when a connection is closed
func (srs *StandardRicochetService) OnDisconnect(oc *OpenConnection) {
}
// OnAuthenticationRequest is called when a client requests Authentication
func (srs *StandardRicochetService) OnAuthenticationRequest(oc *OpenConnection, channelID int32, clientCookie [16]byte) {
oc.ConfirmAuthChannel(channelID, clientCookie)
}
// OnAuthenticationChallenge constructs a valid authentication challenge to the serverCookie
func (srs *StandardRicochetService) OnAuthenticationChallenge(oc *OpenConnection, channelID int32, serverCookie [16]byte) {
// DER Encode the Public Key
publickeyBytes, _ := asn1.Marshal(rsa.PublicKey{
N: srs.privateKey.PublicKey.N,
E: srs.privateKey.PublicKey.E,
})
oc.SendProof(1, serverCookie, publickeyBytes, srs.privateKey)
}
// OnAuthenticationProof is called when a client sends Proof for an existing authentication challenge
func (srs *StandardRicochetService) OnAuthenticationProof(oc *OpenConnection, channelID int32, publicKey []byte, signature []byte, isKnownContact bool) {
result := oc.ValidateProof(channelID, publicKey, signature)
oc.SendAuthenticationResult(channelID, result, isKnownContact)
oc.IsAuthed = result
oc.CloseChannel(channelID)
}
// OnAuthenticationResult is called once a server has returned the result of the Proof Verification
func (srs *StandardRicochetService) OnAuthenticationResult(oc *OpenConnection, channelID int32, result bool, isKnownContact bool) {
oc.IsAuthed = result
}
// IsKnownContact allows a caller to determine if a hostname an authorized contact.
func (srs *StandardRicochetService) IsKnownContact(hostname string) bool {
return false
}
// OnContactRequest is called when a client sends a new contact request
func (srs *StandardRicochetService) OnContactRequest(oc *OpenConnection, channelID int32, nick string, message string) {
}
// OnContactRequestAck is called when a server sends a reply to an existing contact request
func (srs *StandardRicochetService) OnContactRequestAck(oc *OpenConnection, channelID int32, status string) {
}
// OnOpenChannelRequest is called when a client or server requests to open a new channel
func (srs *StandardRicochetService) OnOpenChannelRequest(oc *OpenConnection, channelID int32, channelType string) {
oc.AckOpenChannel(channelID, channelType)
}
// OnOpenChannelRequestSuccess is called when a client or server responds to an open channel request
func (srs *StandardRicochetService) OnOpenChannelRequestSuccess(oc *OpenConnection, channelID int32) {
}
// OnChannelClosed is called when a client or server closes an existing channel
func (srs *StandardRicochetService) OnChannelClosed(oc *OpenConnection, channelID int32) {
}
// OnChatMessage is called when a new chat message is received.
func (srs *StandardRicochetService) OnChatMessage(oc *OpenConnection, channelID int32, messageID int32, message string) {
oc.AckChatMessage(channelID, messageID)
}
// OnChatMessageAck is called when a new chat message is ascknowledged.
func (srs *StandardRicochetService) OnChatMessageAck(oc *OpenConnection, channelID int32, messageID int32) {
}
// OnFailedChannelOpen is called when a server fails to open a channel
func (srs *StandardRicochetService) OnFailedChannelOpen(oc *OpenConnection, channelID int32, errorType string) {
oc.UnsetChannel(channelID)
}
// OnGenericError is called when a generalized error is returned from the peer
func (srs *StandardRicochetService) OnGenericError(oc *OpenConnection, channelID int32) {
oc.RejectOpenChannel(channelID, "GenericError")
}
//OnUnknownTypeError is called when an unknown type error is returned from the peer
func (srs *StandardRicochetService) OnUnknownTypeError(oc *OpenConnection, channelID int32) {
oc.RejectOpenChannel(channelID, "UnknownTypeError")
}
// OnUnauthorizedError is called when an unathorized error is returned from the peer
func (srs *StandardRicochetService) OnUnauthorizedError(oc *OpenConnection, channelID int32) {
oc.RejectOpenChannel(channelID, "UnauthorizedError")
}
// OnBadUsageError is called when a bad usage error is returned from the peer
func (srs *StandardRicochetService) OnBadUsageError(oc *OpenConnection, channelID int32) {
oc.RejectOpenChannel(channelID, "BadUsageError")
}
// OnFailedError is called when a failed error is returned from the peer
func (srs *StandardRicochetService) OnFailedError(oc *OpenConnection, channelID int32) {
oc.RejectOpenChannel(channelID, "FailedError")
}

View File

@ -1,108 +0,0 @@
package goricochet
import "testing"
import "time"
import "log"
type TestBadUsageService struct {
StandardRicochetService
BadUsageErrorCount int
UnknownTypeErrorCount int
ChannelClosed int
}
func (ts *TestBadUsageService) OnConnect(oc *OpenConnection) {
if oc.Client {
oc.OpenChannel(17, "im.ricochet.auth.hidden-service") // Fail because no Extension
}
ts.StandardRicochetService.OnConnect(oc)
if oc.Client {
oc.Authenticate(103) // Should Fail because cannot open more than one auth-hidden-service channel at once
}
}
func (ts *TestBadUsageService) OnAuthenticationProof(oc *OpenConnection, channelID int32, publicKey []byte, signature []byte, isKnownContact bool) {
oc.Authenticate(2) // Try to authenticate again...will fail servers don't auth
oc.SendContactRequest(4, "test", "test") // Only clients can send contact requests
ts.StandardRicochetService.OnAuthenticationProof(oc, channelID, publicKey, signature, isKnownContact)
oc.OpenChatChannel(5) // Fail because server can only open even numbered channels
oc.OpenChatChannel(3) // Fail because already in use...
}
// OnContactRequest is called when a client sends a new contact request
func (ts *TestBadUsageService) OnContactRequest(oc *OpenConnection, channelID int32, nick string, message string) {
oc.AckContactRequestOnResponse(channelID, "Pending") // Done to keep the contact request channel open
}
func (ts *TestBadUsageService) OnAuthenticationResult(oc *OpenConnection, channelID int32, result bool, isKnownContact bool) {
ts.StandardRicochetService.OnAuthenticationResult(oc, channelID, result, isKnownContact)
oc.OpenChatChannel(3) // Succeed
oc.OpenChatChannel(3) // Should fail as duplicate (channel already in use)
oc.OpenChatChannel(6) // Should fail because clients are not allowed to open even numbered channels
oc.SendMessage(101, "test") // Should fail as 101 doesn't exist
oc.Authenticate(1) // Try to authenticate again...will fail because we have already authenticated
oc.OpenChannel(19, "im.ricochet.contact.request") // Will Fail
oc.SendContactRequest(11, "test", "test") // Succeed
oc.SendContactRequest(13, "test", "test") // Trigger singleton contact request check
oc.OpenChannel(15, "im.ricochet.not-a-real-type") // Fail UnknownType
}
// OnChannelClose is called when a client or server closes an existing channel
func (ts *TestBadUsageService) OnChannelClosed(oc *OpenConnection, channelID int32) {
if channelID == 101 {
log.Printf("Received Channel Closed: %v", channelID)
ts.ChannelClosed++
}
}
func (ts *TestBadUsageService) OnFailedChannelOpen(oc *OpenConnection, channelID int32, errorType string) {
log.Printf("Failed Channel Open %v %v", channelID, errorType)
ts.StandardRicochetService.OnFailedChannelOpen(oc, channelID, errorType)
if errorType == "BadUsageError" {
ts.BadUsageErrorCount++
} else if errorType == "UnknownTypeError" {
ts.UnknownTypeErrorCount++
}
}
func (ts *TestBadUsageService) IsKnownContact(hostname string) bool {
return true
}
func TestBadUsageServer(t *testing.T) {
ricochetService := new(TestBadUsageService)
err := ricochetService.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService.Listen(ricochetService, 9884)
time.Sleep(time.Second * 2)
ricochetService2 := new(TestBadUsageService)
err = ricochetService2.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService2.Listen(ricochetService2, 9885)
err = ricochetService2.Connect("127.0.0.1:9884|kwke2hntvyfqm7dr")
if err != nil {
t.Errorf("Could not connect to ricochet service: %v", err)
}
time.Sleep(time.Second * 3)
if ricochetService2.ChannelClosed != 1 || ricochetService2.BadUsageErrorCount != 7 || ricochetService.BadUsageErrorCount != 4 || ricochetService2.UnknownTypeErrorCount != 1 {
t.Errorf("Invalid number of errors seen Closed:%v, Client Bad Usage:%v UnknownTypeErrorCount: %v, Server Bad Usage: %v ", ricochetService2.ChannelClosed, ricochetService2.BadUsageErrorCount, ricochetService2.UnknownTypeErrorCount, ricochetService.BadUsageErrorCount)
}
}

View File

@ -1,107 +0,0 @@
package goricochet
import "testing"
import "time"
import "log"
type TestService struct {
StandardRicochetService
ReceivedMessage bool
KnownContact bool // Mocking contact request
}
func (ts *TestService) OnAuthenticationResult(oc *OpenConnection, channelID int32, result bool, isKnownContact bool) {
ts.StandardRicochetService.OnAuthenticationResult(oc, channelID, result, isKnownContact)
if !isKnownContact {
log.Printf("Sending Contact Request")
oc.SendContactRequest(3, "test", "test")
}
}
func (ts *TestService) OnContactRequest(oc *OpenConnection, channelID int32, nick string, message string) {
ts.StandardRicochetService.OnContactRequest(oc, channelID, nick, message)
oc.AckContactRequestOnResponse(channelID, "Pending")
oc.AckContactRequest(channelID, "Accepted")
ts.KnownContact = true
oc.CloseChannel(channelID)
}
func (ts *TestService) OnOpenChannelRequestSuccess(oc *OpenConnection, channelID int32) {
ts.StandardRicochetService.OnOpenChannelRequestSuccess(oc, channelID)
oc.SendMessage(channelID, "TEST MESSAGE")
}
func (ts *TestService) OnContactRequestAck(oc *OpenConnection, channelID int32, status string) {
ts.StandardRicochetService.OnContactRequestAck(oc, channelID, status)
if status == "Accepted" {
log.Printf("Got accepted contact request")
ts.KnownContact = true
oc.OpenChatChannel(5)
} else if status == "Pending" {
log.Printf("Got pending contact request")
}
}
func (ts *TestService) OnChatMessage(oc *OpenConnection, channelID int32, messageID int32, message string) {
ts.StandardRicochetService.OnChatMessage(oc, channelID, messageID, message)
if message == "TEST MESSAGE" {
ts.ReceivedMessage = true
}
}
func (ts *TestService) IsKnownContact(hostname string) bool {
return ts.KnownContact
}
func TestServer(t *testing.T) {
ricochetService := new(TestService)
err := ricochetService.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService.Listen(ricochetService, 9878)
time.Sleep(time.Second * 2)
ricochetService2 := new(TestService)
err = ricochetService2.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService2.Listen(ricochetService2, 9879)
err = ricochetService2.Connect("127.0.0.1:9878|kwke2hntvyfqm7dr")
if err != nil {
t.Errorf("Could not connect to ricochet service: %v", err)
}
time.Sleep(time.Second * 5) // Wait a bit longer
if !ricochetService.ReceivedMessage {
t.Errorf("Test server did not receive message")
}
}
func TestServerInvalidKey(t *testing.T) {
ricochetService := new(TestService)
err := ricochetService.Init("./private_key.does.not.exist")
if err == nil {
t.Errorf("Should not have initate ricochet service, private key should not exist")
}
}
func TestServerCouldNotConnect(t *testing.T) {
ricochetService := new(TestService)
err := ricochetService.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
err = ricochetService.Connect("127.0.0.1:65535|kwke2hntvyfqm7dr")
if err == nil {
t.Errorf("Should not have been been able to connect to 127.0.0.1:65535|kwke2hntvyfqm7dr")
}
}

View File

@ -1,63 +0,0 @@
package goricochet
import "testing"
import "time"
import "log"
// The purpose of this test is to exercise the Unauthorized Error flows that occur
// when a client attempts to open a Chat Channel or Send a Contact Reuqest before Authentication
// itself with the Service.
type TestUnauthorizedService struct {
StandardRicochetService
FailedToOpen int
}
func (ts *TestUnauthorizedService) OnConnect(oc *OpenConnection) {
if oc.Client {
log.Printf("Attempting Authentication Not Authorized")
oc.IsAuthed = true // Connections to Servers are Considered Authenticated by Default
// REMOVED Authenticate
oc.OpenChatChannel(5)
oc.SendContactRequest(3, "test", "test")
}
}
func (ts *TestUnauthorizedService) OnFailedChannelOpen(oc *OpenConnection, channelID int32, errorType string) {
oc.UnsetChannel(channelID)
if errorType == "UnauthorizedError" {
ts.FailedToOpen++
}
}
func TestUnauthorizedClientReject(t *testing.T) {
ricochetService := new(TestService)
err := ricochetService.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService.Listen(ricochetService, 9880)
time.Sleep(time.Second * 2)
ricochetService2 := new(TestUnauthorizedService)
err = ricochetService2.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService2.Listen(ricochetService2, 9881)
err = ricochetService2.Connect("127.0.0.1:9880|kwke2hntvyfqm7dr")
if err != nil {
t.Errorf("Could not connect to ricochet service: %v", err)
}
time.Sleep(time.Second * 2)
if ricochetService2.FailedToOpen != 2 {
t.Errorf("Test server did not reject open channels with unauthorized error")
}
}

View File

@ -1,60 +0,0 @@
package goricochet
import "testing"
import "time"
import "log"
type TestUnknownContactService struct {
StandardRicochetService
FailedToOpen bool
}
func (ts *TestUnknownContactService) OnAuthenticationResult(oc *OpenConnection, channelID int32, result bool, isKnownContact bool) {
log.Printf("Authentication Result")
ts.StandardRicochetService.OnAuthenticationResult(oc, channelID, result, isKnownContact)
oc.OpenChatChannel(5)
}
func (ts *TestUnknownContactService) OnFailedChannelOpen(oc *OpenConnection, channelID int32, errorType string) {
log.Printf("Failed Channel Open %v", errorType)
oc.UnsetChannel(channelID)
if errorType == "UnauthorizedError" {
ts.FailedToOpen = true
}
}
func (ts *TestUnknownContactService) IsKnownContact(hostname string) bool {
return false
}
func TestUnknownContactServer(t *testing.T) {
ricochetService := new(StandardRicochetService)
err := ricochetService.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService.Listen(ricochetService, 9882)
time.Sleep(time.Second * 2)
ricochetService2 := new(TestUnknownContactService)
err = ricochetService2.Init("./private_key")
if err != nil {
t.Errorf("Could not initate ricochet service: %v", err)
}
go ricochetService2.Listen(ricochetService2, 9883)
err = ricochetService2.Connect("127.0.0.1:9882|kwke2hntvyfqm7dr")
if err != nil {
t.Errorf("Could not connect to ricochet service: %v", err)
}
time.Sleep(time.Second * 2)
if !ricochetService2.FailedToOpen {
t.Errorf("Test server did receive message should have failed")
}
}

52
testing.md Normal file
View File

@ -0,0 +1,52 @@
# Ricochet Testing Specification
This documents outlines each scenario this library must correctly handle and
links it to the automated test that exercises that functionality.
# Version Negotiation
## Open
File: [iricochet_test.go](./ricochet_test.go)
This stub test exercises the Open() function. `TestRicochetOpen`, and in cases where the server returns a bad version, `TestRicochetOpenWithError`, and where there is no server at all `TestRicochetOpenWithNoServer` - in both cases Open should return an error.
## Inbound
File: [inbound_version_negotiation_test.go](./inbound_version_negotiation_test.go)
### Invalid Protocol
If the inbound listener receives:
* Less than 4 bytes (`TestBadProtcolLength`)
* The first 2 bytes are not equal ot 0x49 and 0x4D
* A number of supported Versions &lt; 1 (`TestNoSupportedVersions`, `TestInvalidVersionList`)
Then it must close the connection.
### No Compatible Version Found
If the inbound listener does not receive a compatible version in the list of
supported versions. Then is must close the connection. `TestNoCompatibleVersions`
### Successful Version Negotiation
Assuming the inbound listener receives a valid protocol message, and that message
contains a known supported version. Then the connection should remain open. `TestNegotiateInboundVersions`
## Outbound
File: [outbound_version_negotiation_test.go](./outbound_version_negotiation_test.go)
### No Compatible Version Found
If the outbound connection receives a response that does not match one of the versions
they sent out in their supporting list. Then then must close the connection `TestInvalidResponse` , `TestInvalidServer`
### Successful Version Negotiation
Assuming the outbound connection receives a valid protocol message, and that message
contains a known supported version. Then the connection should remain open. `TestOutboundVersionNegotiation`

240
testing/integration_test.go Normal file
View File

@ -0,0 +1,240 @@
package testing
import (
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"runtime"
"strconv"
"sync"
"testing"
"time"
)
type Message struct {
From, To string
Message string
}
type MessageStack interface {
Add(from, to, message string)
Get() []Message
}
type Messages struct {
messages []Message
lock sync.Mutex
}
func (messages *Messages) Init() {
messages.messages = []Message{}
}
func (messages *Messages) Add(from, to, message string) {
messages.lock.Lock()
messages.messages = append(messages.messages, Message{from, to, message})
messages.lock.Unlock()
}
func (messages *Messages) Get() []Message {
return messages.messages
}
type ChatEchoBot struct {
onion string
rai *application.Instance
n int
Messages MessageStack
}
// We always want bidirectional chat channels
func (bot *ChatEchoBot) OpenInbound() {
log.Infoln("OpenInbound() ChatChannel handler called...")
outboutChatChannel := bot.rai.Connection.Channel("im.ricochet.chat", channels.Outbound)
if outboutChatChannel == nil {
bot.rai.Connection.Do(func() error {
bot.rai.Connection.RequestOpenChannel("im.ricochet.chat",
&channels.ChatChannel{
Handler: bot,
})
return nil
})
}
}
func (bot *ChatEchoBot) ChatMessage(messageID uint32, when time.Time, message string) bool {
log.Infof("ChatMessage(from: %v, %v", bot.rai.RemoteHostname, message)
bot.Messages.Add(bot.rai.RemoteHostname, bot.onion, message)
SendMessage(bot.rai, strconv.Itoa(bot.n)+" witty response")
bot.n++
return true
}
func SendMessage(rai *application.Instance, message string) error {
log.Infof("SendMessage(to: %v, %v)\n", rai.RemoteHostname, message)
return rai.Connection.Do(func() error {
log.Infof("Finding Chat Channel")
channel := rai.Connection.Channel("im.ricochet.chat", channels.Outbound)
_, err := channels.SendMessageOnChatChannel(channel, message)
if err != nil {
log.Infof("Could not find chat channel")
}
return nil
})
}
func (bot *ChatEchoBot) ChatMessageAck(messageID uint32, accepted bool) {
}
func TestApplicationIntegration(t *testing.T) {
log.SetLevel(log.LevelDebug)
startGoRoutines := runtime.NumGoroutine()
acn, err := connectivity.StartTor(".", "")
if err != nil {
t.Fatalf("Could not start tor: %v", err)
}
time.Sleep(1 * time.Second)
acnStartGoRoutines := runtime.NumGoroutine()
messageStack := &Messages{}
messageStack.Init()
fmt.Println("Initializing application factory...")
af := application.InstanceFactory{}
af.Init()
af.AddHandler("im.ricochet.contact.request", func(rai *application.Instance) func() channels.Handler {
return func() channels.Handler {
contact := new(channels.ContactRequestChannel)
contact.Handler = new(application.AcceptAllContactHandler)
return contact
}
})
fmt.Println("Starting alice...")
alice := new(application.RicochetApplication)
fmt.Println("Generating alice's pk...")
apubk, apk, _ := utils.GeneratePrivateKeyV3()
aliceAddr := utils.GetTorV3Hostname(apubk)
fmt.Println("Seting up alice's onion " + aliceAddr + "...")
al, err := acn.Listen(apk, application.RicochetPort)
if err != nil {
t.Fatalf("Could not setup Onion for Alice: %v", err)
}
fmt.Println("Initializing alice...")
af.AddHandler("im.ricochet.chat", func(rai *application.Instance) func() channels.Handler {
return func() channels.Handler {
chat := new(channels.ChatChannel)
chat.Handler = &ChatEchoBot{rai: rai, n: 0, Messages: messageStack, onion: aliceAddr}
return chat
}
})
alice.Init(acn, "Alice", identity.InitializeV3("Alice", &apk, &apubk), af, new(application.AcceptAllContactManager))
fmt.Println("Running alice...")
go alice.Run(al)
fmt.Println("Starting bob...")
bob := new(application.RicochetApplication)
bpubk, bpk, err := utils.GeneratePrivateKeyV3()
if err != nil {
t.Fatalf("Could not setup Onion for Alice: %v", err)
}
bobAddr := utils.GetTorV3Hostname(bpubk)
fmt.Println("Seting up bob's onion " + bobAddr + "...")
bl, _ := acn.Listen(bpk, application.RicochetPort)
af.AddHandler("im.ricochet.chat", func(rai *application.Instance) func() channels.Handler {
return func() channels.Handler {
chat := new(channels.ChatChannel)
chat.Handler = &ChatEchoBot{rai: rai, n: 0, Messages: messageStack, onion: bobAddr}
return chat
}
})
bob.Init(acn, "Bob", identity.InitializeV3("Bob", &bpk, &bpubk), af, new(application.AcceptAllContactManager))
go bob.Run(bl)
fmt.Println("Waiting for alice and bob hidden services to percolate...")
time.Sleep(60 * time.Second)
runningGoRoutines := runtime.NumGoroutine()
fmt.Println("Alice connecting to Bob...")
// out going rc from alice to bob
alicei, err := alice.Open(bobAddr, "It's alice")
if err != nil {
t.Fatalf("Error Alice connecting to Bob: %v", err)
}
time.Sleep(30 * time.Second)
fmt.Println("Alice request open chat channel...")
// TODO: opening a channel should be easier?
err = alicei.Connection.Do(func() error {
handler, err := alicei.OnOpenChannelRequest("im.ricochet.chat")
if err != nil {
log.Infof("Could not get chat handler!\n")
return err
}
_, err = alicei.Connection.RequestOpenChannel("im.ricochet.chat", handler)
return err
})
if err != nil {
t.Errorf("Error Opening a Channel: %v", err)
}
time.Sleep(30 * time.Second)
fmt.Println("Alice sending message to Bob...")
err = SendMessage(alicei, "Hello Bob!")
if err != nil {
t.Errorf("Error dialing from Alice to Bob: %v", err)
}
time.Sleep(10 * time.Second)
// should now be connected to bob
connectedGoRoutines := runtime.NumGoroutine()
fmt.Println("Shutting bob down...")
bob.Shutdown()
time.Sleep(15 * time.Second)
bobShutdownGoRoutines := runtime.NumGoroutine()
fmt.Println("Shutting alice down...")
alice.Shutdown()
time.Sleep(15 * time.Second)
aliceShutdownGoRoutines := runtime.NumGoroutine()
fmt.Println("Shutting down bine/tor")
acn.Close()
time.Sleep(5 * time.Second)
finalGoRoutines := runtime.NumGoroutine()
fmt.Printf("startGoRoutines: %v\nacnStartedGoRoutines: %v\nrunningGoRoutines: %v\nconnectedGoRoutines: %v\nBobShutdownGoRoutines: %v\naliceShutdownGoRoutines: %v\nfinalGoRoutines: %v\n", startGoRoutines, acnStartGoRoutines, runningGoRoutines, connectedGoRoutines, bobShutdownGoRoutines, aliceShutdownGoRoutines, finalGoRoutines)
if finalGoRoutines != startGoRoutines {
t.Errorf("After shutting alice and bob down, go routines were not at start value. Expected: %v Actual: %v", startGoRoutines, finalGoRoutines)
}
fmt.Println("Messages:")
for _, message := range messageStack.Get() {
fmt.Printf(" from:%v to:%v '%v'\n", message.From, message.To, message.Message)
}
messages := messageStack.Get()
if messages[0].Message != "Hello Bob!" || messages[1].Message != "0 witty response" {
t.Errorf("Message history did not contain first two expected messages!")
}
}

15
testing/private_key Normal file
View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQC3xEJBH4oVFaotPJw6dezx67Gv4Xukw8CZRGqNFO8yF7Rejtcj
/0RTqqZwj6H6FjxY60dgYnN6IphW0juemNZhxOXeM/5Gb5xO+kWGi5Qt87aSDxnA
MDLgqw79ihuD3m1C1TBz0olmjXPU1VtadZuZcVBST7SLs2/k55GNNr7BoQIDAQAB
AoGBAK3ybVCdnSQWLM7DJ5LC23Wnx7sXceVlkiLCOyWuYjiFbatwBD/DupaD2yaD
HyzN7XOxyg93QZ2jr5XHTL30KEAn/3akNBsX3sjHZnjVfTwD5+oZKd7HYMMxekWf
87TIx2IHvGEo2NaFMLkEZ5TX3Gre8CYOofjFcpj4661ZfYp9AkEA9I0EmQX26ibs
CRGkwPuEj5q5N/PmIHgMWr1pepOlmzJjnxy6SI3NUwmzKrqM6YUM8loSywqfVMrJ
RVzA5jp76wJBAMBeu2hS8KcUTIu66j0pXMhI5wDA3yLiO53TEMwufCPXcaWUMH+e
5AIPL7aZ8ouf895OH0TZKxPNMnbrJ+5F0aMCQDoi/CDUxipMLnjJdP1bzdvF0Jp4
pRC6+VTpCpZVW11V0VEWJ0LwUwuWlr1ls/If60ACIc2bLN2fh9Gxhzo0VRkCQQCS
nKCAVhYLgLEGHaLAknGgQ8+rB1QIphuBoYc/1n3OYzi+VT7RRSvJVgGrTZFJUNLw
LuIt+sWWBeHcOETqmFO5AkEAwwfcxs8QZtX6hCj2MTPi8Q28LIoA/M6eAqYc2I0B
eXxf2J2Qco7sMmBLr1Jp3jZNd5W2fMtlhUZAomOj4piVOA==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC9eXEz2sONLCHcaW3OR2kB1fwp+DkQYC74J4FkrdbuSLoPi/fZ
l0bRQZXKprZGhQsH0z1ERuD5wJD/XDws3XdIJuiGw8wEttwFe8lbsBsRedmjqsAy
NukE1gZDoVYAwYgyLz7Obch7m+2h4M42uMDzyGno4nXKIV/1hTfLJvqw6QIDAQAB
AoGADp+Kzxe5M/IOAvbYFK2KOywKtCqGLO9fcKOL5vtLtURDp+ODk3WLb6cCKovH
UZX/DfGNrvFRd7UW+75gno3RIMxbdyC8AcKNz8jnYzSpG2/tXL8LNAZxV5OdbxG3
S2iVB/rOt49ilH2WcaqUkSqL0+goPLcJy2k/owV0aPEOUwECQQDsTdHbkYt7cSKn
aJtIRV1j3M1Tzu7ZJYLzDF5S0VECP80Gb9gCpMPSt45hGk6AzMGZFCImi9vmiW2c
TzFgLHbZAkEAzURjG0o9YRhesZkg+PoJ33zakg+Tp/6FYY73eBqLg71iO2YS9YIR
DwJ9IG//V8oqFm0dhW20LLbvTqtWyspgkQJBAN5ai7I0Ti+l0Zn9kMB8pNgnGP5X
peCmr4XMiaUcWUHojyATdgtmxu0s08kDXANOqI1GqKvkxtMzVfTTf/6jWGECQQCY
e3DT2PZ3pk7Rx1sDGVs0Nd94GTIq3ZvfuQCEq9Nv7cOHNHBpCFH7wHGLIyef44IY
Xr5LXA84GDz1R7qVsnjBAkB1qYel38r3NoMvVLhCUh2HLZSTxPF9V7iE+5OvakIJ
+Glb45PyloFIobv1yQoIOJlu+uoilGRbOiMUVG1uS0Tj
-----END RSA PRIVATE KEY-----

23
testing/quality.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/sh
echo "Checking code quality (you want to see no output here)"
echo "Formatting:"
gofmt -s -w -l .
echo "Vetting:"
go list ./... | xargs go vet
echo ""
echo "Linting:"
# Ignore wire packages as they are autogenerated
go list ./... | grep -v "/wire/" | xargs golint
# ineffassign (https://github.com/gordonklaus/ineffassign)
echo "Checking for ineffectual assignment of errors (unchecked errors...)"
ineffassign .
# misspell (https://github.com/client9/misspell)
echo "Checking for misspelled words..."
go list ./... | grep -v "/wire/" | grep -v "/vendor/" | xargs misspell

18
testing/tests.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
#go test ${1} -coverprofile=coverage.out -coverpkg="git.openprivacy.ca/openprivacy/libricochet-go/channels,git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/inbound,git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/outbound,git.openprivacy.ca/openprivacy/libricochet-go/identity,git.openprivacy.ca/openprivacy/libricochet-go/utils,git.openprivacy.ca/openprivacy/libricochet-go/policies,git.openprivacy.ca/openprivacy/libricochet-go/connection,git.openprivacy.ca/openprivacy/libricochet-go" -v . ./utils/ ./channels/ ./channels/v3/inbound ./connection ./policies ./identity ./utils
set -e
pwd
GORACE="haltonerror=1"
go test -race ${1} -coverprofile=utils.cover.out -v ./utils
go test -race ${1} -coverprofile=channels.cover.out -v ./channels
go test -race ${1} -coverprofile=channels.v3.inbound.cover.out -v ./channels/v3/inbound
go test -race ${1} -coverprofile=connection.cover.out -v ./connection
go test -race ${1} -coverprofile=policies.cover.out -v ./policies
go test -race ${1} -coverprofile=identity.cover.out -v ./identity
go test -race ${1} -coverprofile=root.cover.out -v ./
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -e
pwd
go test -coverprofile=main.cover.out -v .
go test -coverprofile=utils.cover.out -v ./utils
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out

83
utils/crypto.go Normal file
View File

@ -0,0 +1,83 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"github.com/agl/ed25519/extra25519"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"math"
"math/big"
)
const (
// InvalidPrivateKeyFileError is a library error, thrown when the given key file fials to load
InvalidPrivateKeyFileError = Error("InvalidPrivateKeyFileError")
// RicochetKeySize - tor onion services currently use rsa key sizes of 1024 bits
RicochetKeySize = 1024
)
// GetRandNumber is a helper function which returns a random integer, this is
// currently mostly used to generate messageids
func GetRandNumber() *big.Int {
num, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
// If we can't generate random numbers then panicking is probably
// the best option.
CheckError(err)
return num
}
// EDH implements diffie hellman using curve25519 keys derived from ed25519 keys
// NOTE: This uses a 3rd party library extra25519 as the key conversion is not in the core golang lib
// as such this definitely needs further review.
func EDH(privateKey ed25519.PrivateKey, remotePublicKey ed25519.PublicKey) [32]byte {
var privKeyBytes [64]byte
var remotePubKeyBytes [32]byte
copy(privKeyBytes[:], privateKey[:])
copy(remotePubKeyBytes[:], remotePublicKey[:])
var secret, curve25519priv, curve25519pub [32]byte
extra25519.PrivateKeyToCurve25519(&curve25519priv, &privKeyBytes)
extra25519.PublicKeyToCurve25519(&curve25519pub, &remotePubKeyBytes)
curve25519.ScalarMult(&secret, &curve25519priv, &curve25519pub)
return secret
}
// GeneratePrivateKeyV3 cryptographically creats a new ed25519 key pair.
func GeneratePrivateKeyV3() (ed25519.PublicKey, ed25519.PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
// LoadPrivateKeyFromFile loads a private key from a file...
func LoadPrivateKeyFromFile(filename string) (*rsa.PrivateKey, error) {
pemData, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return ParsePrivateKey(pemData)
}
// ParsePrivateKey Convert a private key string to a usable private key
func ParsePrivateKey(pemData []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, InvalidPrivateKeyFileError
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
// PrivateKeyToString turns a private key into storable string
func PrivateKeyToString(privateKey *rsa.PrivateKey) string {
privateKeyBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
return string(pem.EncodeToMemory(&privateKeyBlock))
}

36
utils/crypto_test.go Normal file
View File

@ -0,0 +1,36 @@
package utils
import (
"crypto/rand"
"golang.org/x/crypto/ed25519"
"math"
"testing"
)
const (
privateKeyFile = "./../testing/private_key"
)
func TestLoadPrivateKey(t *testing.T) {
_, err := LoadPrivateKeyFromFile(privateKeyFile)
if err != nil {
t.Errorf("Error while loading private key from file: %v", err)
}
}
func TestEDH(t *testing.T) {
cpub, cpriv, _ := ed25519.GenerateKey(rand.Reader)
spub, spriv, _ := ed25519.GenerateKey(rand.Reader)
cedh := EDH(cpriv, spub)
sedh := EDH(spriv, cpub)
if string(cedh[:]) != string(sedh[:]) {
t.Errorf("Client and Server should see the same secret %v %v", cedh, sedh)
}
}
func TestGetRandNumber(t *testing.T) {
num := GetRandNumber()
if !num.IsUint64() || num.Uint64() > uint64(math.MaxUint32) {
t.Errorf("Error random number outside of expected bounds %v", num)
}
}

View File

@ -1,17 +1,50 @@
package utils
import "fmt"
import "log"
import (
"fmt"
)
// RecoverFromError doesn't really recover from anything....see comment below
func RecoverFromError() {
if r := recover(); r != nil {
// This should only really happen if there is a failure de/serializing. If
// this does happen then we currently error. In the future we might be
// able to make this nicer.
log.Fatalf("Recovered from panic() - this really shouldn't happen. Reason: %v", r)
}
}
// Error captures various common ricochet errors
type Error string
func (e Error) Error() string { return string(e) }
// Defining Versions
const (
VersionNegotiationError = Error("VersionNegotiationError")
VersionNegotiationFailed = Error("VersionNegotiationFailed")
RicochetConnectionClosed = Error("RicochetConnectionClosed")
RicochetProtocolError = Error("RicochetProtocolError")
UnknownChannelTypeError = Error("UnknownChannelTypeError")
UnauthorizedChannelTypeError = Error("UnauthorizedChannelTypeError")
UnexpectedChannelResultError = Error("UnexpectedChannelResultError")
// Timeout Errors
ActionTimedOutError = Error("ActionTimedOutError")
PeerTimedOutError = Error("PeerTimedOutError")
// Authentication Errors
ClientFailedToAuthenticateError = Error("ClientFailedToAuthenticateError")
ServerRejectedClientConnectionError = Error("ServerRejectedClientConnectionError")
UnauthorizedActionError = Error("UnauthorizedActionError")
ChannelClosedByPeerError = Error("ChannelClosedByPeerError")
// Channel Management Errors
ServerAttemptedToOpenEvenNumberedChannelError = Error("ServerAttemptedToOpenEvenNumberedChannelError")
ClientAttemptedToOpenOddNumberedChannelError = Error("ClientAttemptedToOpenOddNumberedChannelError")
ChannelIDIsAlreadyInUseError = Error("ChannelIDIsAlreadyInUseError")
AttemptToOpenMoreThanOneSingletonChannelError = Error("AttemptToOpenMoreThanOneSingletonChannelError")
// Library Use Errors
OnionAddressGenerationError = Error("OnionAddressGenerationError")
PrivateKeyNotSetError = Error("PrivateKeyNotSet")
// Connection Errors
ConnectionClosedError = Error("ConnectionClosedError")
)
// CheckError is a helper function for panicing on errors which we need to handle
// but should be very rare e.g. failures deserializing a protobuf object that

View File

@ -1,12 +1,12 @@
package goricochet
package utils
import (
"git.openprivacy.ca/openprivacy/libricochet-go/wire/auth"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/chat"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/contact"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/auth"
"github.com/s-rah/go-ricochet/chat"
"github.com/s-rah/go-ricochet/contact"
"github.com/s-rah/go-ricochet/control"
"github.com/s-rah/go-ricochet/utils"
)
// MessageBuilder allows a client to construct specific data packets for the
@ -16,7 +16,7 @@ type MessageBuilder struct {
// OpenChannel contructs a message which will request to open a channel for
// chat on the given channelID.
func (mb *MessageBuilder) OpenChannel(channelID int32, channelType string) ([]byte, error) {
func (mb *MessageBuilder) OpenChannel(channelID int32, channelType string) []byte {
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(channelID),
ChannelType: proto.String(channelType),
@ -24,11 +24,13 @@ func (mb *MessageBuilder) OpenChannel(channelID int32, channelType string) ([]by
pc := &Protocol_Data_Control.Packet{
OpenChannel: oc,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// AckOpenChannel constructs a message to acknowledge a previous open channel operation.
func (mb *MessageBuilder) AckOpenChannel(channelID int32) ([]byte, error) {
func (mb *MessageBuilder) AckOpenChannel(channelID int32) []byte {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(true),
@ -36,11 +38,13 @@ func (mb *MessageBuilder) AckOpenChannel(channelID int32) ([]byte, error) {
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// RejectOpenChannel constructs a channel result message, stating the channel failed to open and a reason
func (mb *MessageBuilder) RejectOpenChannel(channelID int32, error string) ([]byte, error) {
func (mb *MessageBuilder) RejectOpenChannel(channelID int32, error string) []byte {
errorNum := Protocol_Data_Control.ChannelResult_CommonError_value[error]
commonError := Protocol_Data_Control.ChannelResult_CommonError(errorNum)
@ -53,28 +57,89 @@ func (mb *MessageBuilder) RejectOpenChannel(channelID int32, error string) ([]by
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// ConfirmAuthChannel constructs a message to acknowledge a previous open channel operation.
func (mb *MessageBuilder) ConfirmAuthChannel(channelID int32, serverCookie [16]byte) ([]byte, error) {
func (mb *MessageBuilder) ConfirmAuthChannel(channelID int32, serverCookie [16]byte) []byte {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(true),
}
err := proto.SetExtension(cr, Protocol_Data_AuthHiddenService.E_ServerCookie, serverCookie[:])
utils.CheckError(err)
CheckError(err)
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// Open3EDHAuthenticationChannel constructs a message which will reuqest to open a channel for
// authentication on the given channelID, with the given cookie
func (mb *MessageBuilder) Open3EDHAuthenticationChannel(channelID int32, pubkey [32]byte, ephemeralKey [32]byte) []byte {
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(channelID),
ChannelType: proto.String("im.ricochet.auth.3dh"),
}
err := proto.SetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientPublicKey, pubkey[:])
CheckError(err)
err = proto.SetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientEphmeralPublicKey, ephemeralKey[:])
CheckError(err)
pc := &Protocol_Data_Control.Packet{
OpenChannel: oc,
}
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// Confirm3EDHAuthChannel constructs a message to acknowledge a previous open channel operation.
func (mb *MessageBuilder) Confirm3EDHAuthChannel(channelID int32, pubkey [32]byte, ephemeralKey [32]byte) []byte {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(true),
}
err := proto.SetExtension(cr, Protocol_Data_Auth_TripleEDH.E_ServerPublicKey, pubkey[:])
CheckError(err)
err = proto.SetExtension(cr, Protocol_Data_Auth_TripleEDH.E_ServerEphmeralPublicKey, ephemeralKey[:])
CheckError(err)
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// Proof3DH constructs a proof message with the given public key and signature.
func (mb *MessageBuilder) Proof3DH(proofBytes []byte) []byte {
proof := &Protocol_Data_Auth_TripleEDH.Proof{
Proof: proofBytes,
}
ahsPacket := &Protocol_Data_Auth_TripleEDH.Packet{
Proof: proof,
}
ret, err := proto.Marshal(ahsPacket)
CheckError(err)
return ret
}
// OpenContactRequestChannel contructs a message which will reuqest to open a channel for
// a contact request on the given channelID, with the given nick and message.
func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string, message string) ([]byte, error) {
func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string, message string) []byte {
// Construct a Contact Request Channel
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(channelID),
@ -87,16 +152,18 @@ func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string
}
err := proto.SetExtension(oc, Protocol_Data_ContactRequest.E_ContactRequest, contactRequest)
utils.CheckError(err)
CheckError(err)
pc := &Protocol_Data_Control.Packet{
OpenChannel: oc,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// ReplyToContactRequestOnResponse constructs a message to acknowledge contact request
func (mb *MessageBuilder) ReplyToContactRequestOnResponse(channelID int32, status string) ([]byte, error) {
func (mb *MessageBuilder) ReplyToContactRequestOnResponse(channelID int32, status string) []byte {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(true),
@ -109,42 +176,49 @@ func (mb *MessageBuilder) ReplyToContactRequestOnResponse(channelID int32, statu
}
err := proto.SetExtension(cr, Protocol_Data_ContactRequest.E_Response, contactRequest)
utils.CheckError(err)
CheckError(err)
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// ReplyToContactRequest constructs a message to acknowledge a contact request
func (mb *MessageBuilder) ReplyToContactRequest(channelID int32, status string) ([]byte, error) {
func (mb *MessageBuilder) ReplyToContactRequest(channelID int32, status string) []byte {
statusNum := Protocol_Data_ContactRequest.Response_Status_value[status]
responseStatus := Protocol_Data_ContactRequest.Response_Status(statusNum)
contactRequest := &Protocol_Data_ContactRequest.Response{
Status: &responseStatus,
}
return proto.Marshal(contactRequest)
ret, err := proto.Marshal(contactRequest)
CheckError(err)
return ret
}
// OpenAuthenticationChannel constructs a message which will reuqest to open a channel for
// authentication on the given channelID, with the given cookie
func (mb *MessageBuilder) OpenAuthenticationChannel(channelID int32, clientCookie [16]byte) ([]byte, error) {
func (mb *MessageBuilder) OpenAuthenticationChannel(channelID int32, clientCookie [16]byte) []byte {
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(channelID),
ChannelType: proto.String("im.ricochet.auth.hidden-service"),
}
err := proto.SetExtension(oc, Protocol_Data_AuthHiddenService.E_ClientCookie, clientCookie[:])
utils.CheckError(err)
CheckError(err)
pc := &Protocol_Data_Control.Packet{
OpenChannel: oc,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// Proof constructs a proof message with the given public key and signature.
func (mb *MessageBuilder) Proof(publicKeyBytes []byte, signatureBytes []byte) ([]byte, error) {
func (mb *MessageBuilder) Proof(publicKeyBytes []byte, signatureBytes []byte) []byte {
proof := &Protocol_Data_AuthHiddenService.Proof{
PublicKey: publicKeyBytes,
Signature: signatureBytes,
@ -155,11 +229,31 @@ func (mb *MessageBuilder) Proof(publicKeyBytes []byte, signatureBytes []byte) ([
Result: nil,
}
return proto.Marshal(ahsPacket)
ret, err := proto.Marshal(ahsPacket)
CheckError(err)
return ret
}
// AuthResult3DH constructs a response to a Proof
func (mb *MessageBuilder) AuthResult3DH(accepted bool, isKnownContact bool) []byte {
// Construct a Result Message
result := &Protocol_Data_Auth_TripleEDH.Result{
Accepted: proto.Bool(accepted),
IsKnownContact: proto.Bool(isKnownContact),
}
ahsPacket := &Protocol_Data_Auth_TripleEDH.Packet{
Proof: nil,
Result: result,
}
ret, err := proto.Marshal(ahsPacket)
CheckError(err)
return ret
}
// AuthResult constructs a response to a Proof
func (mb *MessageBuilder) AuthResult(accepted bool, isKnownContact bool) ([]byte, error) {
func (mb *MessageBuilder) AuthResult(accepted bool, isKnownContact bool) []byte {
// Construct a Result Message
result := &Protocol_Data_AuthHiddenService.Result{
Accepted: proto.Bool(accepted),
@ -171,29 +265,75 @@ func (mb *MessageBuilder) AuthResult(accepted bool, isKnownContact bool) ([]byte
Result: result,
}
return proto.Marshal(ahsPacket)
ret, err := proto.Marshal(ahsPacket)
CheckError(err)
return ret
}
// ChatMessage constructs a chat message with the given content.
func (mb *MessageBuilder) ChatMessage(message string, messageID int32) ([]byte, error) {
func (mb *MessageBuilder) ChatMessage(message string, messageID uint32, timeDelta int64) []byte {
cm := &Protocol_Data_Chat.ChatMessage{
MessageId: proto.Uint32(uint32(messageID)),
MessageId: proto.Uint32(messageID),
MessageText: proto.String(message),
TimeDelta: proto.Int64(timeDelta),
}
chatPacket := &Protocol_Data_Chat.Packet{
ChatMessage: cm,
}
return proto.Marshal(chatPacket)
ret, err := proto.Marshal(chatPacket)
CheckError(err)
return ret
}
// AckChatMessage constructs a chat message acknowledgement.
func (mb *MessageBuilder) AckChatMessage(messageID int32) ([]byte, error) {
func (mb *MessageBuilder) AckChatMessage(messageID uint32, accepted bool) []byte {
cr := &Protocol_Data_Chat.ChatAcknowledge{
MessageId: proto.Uint32(uint32(messageID)),
Accepted: proto.Bool(true),
MessageId: proto.Uint32(messageID),
Accepted: proto.Bool(accepted),
}
pc := &Protocol_Data_Chat.Packet{
ChatAcknowledge: cr,
}
return proto.Marshal(pc)
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// KeepAlive ...
func (mb *MessageBuilder) KeepAlive(responseRequested bool) []byte {
ka := &Protocol_Data_Control.KeepAlive{
ResponseRequested: proto.Bool(responseRequested),
}
pc := &Protocol_Data_Control.Packet{
KeepAlive: ka,
}
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// EnableFeatures ...
func (mb *MessageBuilder) EnableFeatures(features []string) []byte {
ef := &Protocol_Data_Control.EnableFeatures{
Feature: features,
}
pc := &Protocol_Data_Control.Packet{
EnableFeatures: ef,
}
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}
// FeaturesEnabled ...
func (mb *MessageBuilder) FeaturesEnabled(features []string) []byte {
fe := &Protocol_Data_Control.FeaturesEnabled{
Feature: features,
}
pc := &Protocol_Data_Control.Packet{
FeaturesEnabled: fe,
}
ret, err := proto.Marshal(pc)
CheckError(err)
return ret
}

View File

@ -0,0 +1,125 @@
package utils
import (
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
)
func TestOpenChatChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.OpenChannel(1, "im.ricochet.chat")
// TODO: More Indepth Test Of Output
}
func TestOpenContactRequestChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.OpenContactRequestChannel(3, "Nickname", "Message")
// TODO: More Indepth Test Of Output
}
func TestOpenAuthenticationChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.OpenAuthenticationChannel(1, [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
// TODO: More Indepth Test Of Output
}
func TestAckOpenChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.AckOpenChannel(1)
// TODO: More Indepth Test Of Output
}
func TestAuthProof(t *testing.T) {
messageBuilder := new(MessageBuilder)
key := make([]byte, 32)
proof := make([]byte, 32)
messageBuilder.Proof(key, proof)
// TODO: More Indepth Test Of Output
}
func TestAuthResult(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.AuthResult(true, true)
// TODO: More Indepth Test Of Output
}
func TestConfirmAuthChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
cookie := [16]byte{}
messageBuilder.ConfirmAuthChannel(0, cookie)
// TODO: More Indepth Test Of Output
}
func TestChatMessage(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.ChatMessage("Hello World", 0, 0)
// TODO: More Indepth Test Of Output
}
func TestRejectOpenChannel(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.RejectOpenChannel(1, "error")
// TODO: More Indepth Test Of Output
}
func TestAckChatMessage(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.AckChatMessage(1, true)
// TODO: More Indepth Test Of Output
}
func TestReplyToContactRequestOnResponse(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.ReplyToContactRequestOnResponse(1, "Accepted")
// TODO: More Indepth Test Of Output
}
func TestReplyToContactRequest(t *testing.T) {
messageBuilder := new(MessageBuilder)
messageBuilder.ReplyToContactRequest(1, "Accepted")
// TODO: More Indepth Test Of Output
}
func TestKeepAlive(t *testing.T) {
messageBuilder := new(MessageBuilder)
raw := messageBuilder.KeepAlive(true)
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(raw, res)
if err != nil || res.GetKeepAlive() == nil || !res.GetKeepAlive().GetResponseRequested() {
t.Errorf("Decoding Keep Alive Packet failed or no response requested: %v %v", err, res)
}
}
func TestFeaturesEnabled(t *testing.T) {
messageBuilder := new(MessageBuilder)
features := []string{"feature1", "feature2"}
raw := messageBuilder.FeaturesEnabled(features)
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(raw, res)
if err != nil || res.GetFeaturesEnabled() == nil {
t.Errorf("Decoding FeaturesEnabled Packet failed: %v %v", err, res)
}
for i, v := range res.GetFeaturesEnabled().GetFeature() {
if v != features[i] {
t.Errorf("Requested Features do not match %v %v", res.GetFeaturesEnabled().GetFeature(), features)
}
}
}
func TestEnableFeatures(t *testing.T) {
messageBuilder := new(MessageBuilder)
features := []string{"feature1", "feature2"}
raw := messageBuilder.EnableFeatures(features)
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(raw, res)
if err != nil || res.GetEnableFeatures() == nil {
t.Errorf("Decoding EnableFeatures Packet failed: %v %v", err, res)
}
for i, v := range res.GetEnableFeatures().GetFeature() {
if v != features[i] {
t.Errorf("Requested Features do not match %v %v", res.GetFeaturesEnabled().GetFeature(), features)
}
}
}

View File

@ -2,9 +2,21 @@ package utils
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/nacl/secretbox"
"io"
"sync"
)
const (
// InvalidPacketLengthError is returned whenever ricochet receives a packet too small or too large to conform to the spec.
InvalidPacketLengthError = Error("InvalidPacketLengthError")
// InvalidChannelIDError channels must be between 0 and 65535
InvalidChannelIDError = Error("InvalidChannelIDError")
)
// RicochetData is a structure containing the raw data and the channel it the
@ -14,7 +26,7 @@ type RicochetData struct {
Data []byte
}
//Equals compares a RicochetData object to another and returns true if contain
// Equals compares a RicochetData object to another and returns true if contain
// the same data.
func (rd RicochetData) Equals(other RicochetData) bool {
return rd.Channel == other.Channel && bytes.Equal(rd.Data, other.Data)
@ -29,6 +41,20 @@ type RicochetNetworkInterface interface {
// RicochetNetwork is a concrete implementation of the RicochetNetworkInterface
type RicochetNetwork struct {
// Derived ephemeral session key for connection
key [32]byte
encrypt bool
lock sync.Mutex
}
// SetEncryptionKey sets the ephemeral encryption key for this session.
func (rn *RicochetNetwork) SetEncryptionKey(key [32]byte) {
rn.lock.Lock()
defer rn.lock.Unlock()
log.Debugf("turning on ephemeral session encryption for connection")
copy(rn.key[:], key[:])
rn.encrypt = true
}
// SendRicochetPacket places the data into a structure needed for the client to
@ -36,15 +62,28 @@ type RicochetNetwork struct {
func (rn *RicochetNetwork) SendRicochetPacket(dst io.Writer, channel int32, data []byte) error {
packet := make([]byte, 4+len(data))
if len(packet) > 65535 {
return errors.New("packet too large")
return InvalidPacketLengthError
}
binary.BigEndian.PutUint16(packet[0:2], uint16(len(packet)))
if channel < 0 || channel > 65535 {
return errors.New("invalid channel ID")
return InvalidChannelIDError
}
binary.BigEndian.PutUint16(packet[2:4], uint16(channel))
copy(packet[4:], data[:])
rn.lock.Lock()
if rn.encrypt {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
encrypted := secretbox.Seal(nonce[:], packet[2:], &nonce, &rn.key)
binary.BigEndian.PutUint16(packet[0:2], uint16(len(encrypted)+2))
packet = append(packet[0:2], encrypted...)
}
rn.lock.Unlock()
for pos := 0; pos < len(packet); {
n, err := dst.Write(packet[pos:])
if err != nil {
@ -52,31 +91,53 @@ func (rn *RicochetNetwork) SendRicochetPacket(dst io.Writer, channel int32, data
}
pos += n
}
return nil
}
// RecvRicochetPacket returns the next packet from reader as a RicochetData
// structure, or an error.
func (rn *RicochetNetwork) RecvRicochetPacket(reader io.Reader) (RicochetData, error) {
packet := RicochetData{}
// Read the four-byte header to get packet length
header := make([]byte, 4)
header := make([]byte, 2)
if _, err := io.ReadAtLeast(reader, header, len(header)); err != nil {
return packet, err
}
size := int(binary.BigEndian.Uint16(header[0:2]))
if size < 4 {
return packet, errors.New("invalid packet length")
return packet, InvalidPacketLengthError
}
packet.Channel = int32(binary.BigEndian.Uint16(header[2:4]))
packet.Data = make([]byte, size-4)
if _, err := io.ReadAtLeast(reader, packet.Data, len(packet.Data)); err != nil {
packetBytes := make([]byte, size-2)
_, err := io.ReadAtLeast(reader, packetBytes, size-2)
if err != nil {
return packet, err
}
rn.lock.Lock()
if rn.encrypt {
var decryptNonce [24]byte
if len(packetBytes) > 24 {
copy(decryptNonce[:], packetBytes[:24])
decrypted, ok := secretbox.Open(nil, packetBytes[24:], &decryptNonce, &rn.key)
if ok {
packetBytes = decrypted
} else {
return packet, errors.New("failed to decrypt encrypted ricochet packet")
}
} else {
return packet, errors.New("ciphertext length was too short")
}
}
rn.lock.Unlock()
packet.Channel = int32(binary.BigEndian.Uint16(packetBytes[0:2]))
packet.Data = make([]byte, len(packetBytes)-2)
copy(packet.Data[:], packetBytes[2:])
return packet, nil
}

View File

@ -1,52 +0,0 @@
package utils
import (
"errors"
"golang.org/x/net/proxy"
"net"
"strings"
)
// NetworkResolver allows a client to resolve various hostnames to connections
// The supported types are onions address are:
// * ricochet:jlq67qzo6s4yp3sp
// * jlq67qzo6s4yp3sp
// * 127.0.0.1:55555|jlq67qzo6s4yp3sp - Localhost Connection
type NetworkResolver struct {
}
// Resolve takes a hostname and returns a net.Conn to the derived endpoint
func (nr *NetworkResolver) Resolve(hostname string) (net.Conn, string, error) {
if strings.HasPrefix(hostname, "127.0.0.1") {
addrParts := strings.Split(hostname, "|")
tcpAddr, err := net.ResolveTCPAddr("tcp", addrParts[0])
if err != nil {
return nil, "", errors.New("Cannot Resolve Local TCP Address")
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
return nil, "", errors.New("Cannot Dial Local TCP Address")
}
// return just the onion address, not the local override for the hostname
return conn, addrParts[1], nil
}
resolvedHostname := hostname
if strings.HasPrefix(hostname, "ricochet:") {
addrParts := strings.Split(hostname, ":")
resolvedHostname = addrParts[1]
}
torDialer, err := proxy.SOCKS5("tcp", "127.0.0.1:9050", nil, proxy.Direct)
if err != nil {
return nil, "", err
}
conn, err := torDialer.Dial("tcp", resolvedHostname+".onion:9878")
if err != nil {
return nil, "", errors.New("Cannot Dial Remote Ricochet Address")
}
//conn.SetDeadline(time.Now().Add(5 * time.Second))
return conn, resolvedHostname, nil
}

View File

@ -2,7 +2,11 @@ package utils
import (
"crypto/sha1"
"crypto/sha512"
"encoding/base32"
"encoding/base64"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
"strings"
)
@ -17,3 +21,57 @@ func GetTorHostname(publicKeyBytes []byte) string {
data := base32.StdEncoding.EncodeToString(sha1bytes)
return strings.ToLower(data[0:16])
}
// Expand ed25519.PrivateKey to (a || RH) form, return base64
func expandKey(pri ed25519.PrivateKey) string {
h := sha512.Sum512(pri[:32])
// Set bits so that h[:32] is private scalar "a"
h[0] &= 248
h[31] &= 127
h[31] |= 64
// Since h[32:] is RH, h is now (a || RH)
return base64.StdEncoding.EncodeToString(h[:])
}
// V3HostnameLength is the length of a Tor V3 Onion Address (without the .onion suffix)
const V3HostnameLength = 56
// Hidden service version
const version = byte(0x03)
// Salt used to create checkdigits
const salt = ".onion checksum"
func getCheckdigits(pub ed25519.PublicKey) []byte {
// Calculate checksum sha3(".onion checksum" || publicKey || version)
checkstr := []byte(salt)
checkstr = append(checkstr, pub...)
checkstr = append(checkstr, version)
checksum := sha3.Sum256(checkstr)
return checksum[:2]
}
// GetTorV3Hostname converts an ed25519 public key to a valid tor onion hostname
func GetTorV3Hostname(pub ed25519.PublicKey) string {
// Construct onion address base32(publicKey || checkdigits || version)
checkdigits := getCheckdigits(pub)
combined := pub[:]
combined = append(combined, checkdigits...)
combined = append(combined, version)
serviceID := base32.StdEncoding.EncodeToString(combined)
return strings.ToLower(serviceID)
}
// IsValidHostname returns true if the given address is a valid onion v3 address
func IsValidHostname(address string) bool {
if len(address) == V3HostnameLength {
data, err := base32.StdEncoding.DecodeString(strings.ToUpper(address))
if err == nil {
pubkey := data[0:ed25519.PublicKeySize]
if GetTorV3Hostname(ed25519.PublicKey(pubkey)) == address {
return true
}
}
}
return false
}

View File

@ -1,10 +1,12 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"golang.org/x/crypto/ed25519"
"testing"
)
@ -41,3 +43,15 @@ func TestGetTorHostname(t *testing.T) {
t.Errorf("Hostname %s does not equal %s", hostname, "kwke2hntvyfqm7dr")
}
}
func TestV3(t *testing.T) {
pub, _, _ := ed25519.GenerateKey(rand.Reader)
hostname := GetTorV3Hostname(pub)
if !IsValidHostname(hostname) {
t.Errorf("Generated V3 Hostname was invalid")
}
if IsValidHostname(hostname[0:34]) {
t.Errorf("Invalid V3 Hostname was marked valid")
}
}

View File

@ -0,0 +1,135 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: Auth3EDH.proto
/*
Package Protocol_Data_Auth_TripleEDH is a generated protocol buffer package.
It is generated from these files:
Auth3EDH.proto
It has these top-level messages:
Packet
Proof
Result
*/
package Protocol_Data_Auth_TripleEDH
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import Protocol_Data_Control "git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type Packet struct {
Proof *Proof `protobuf:"bytes,1,opt,name=proof" json:"proof,omitempty"`
Result *Result `protobuf:"bytes,2,opt,name=result" json:"result,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Packet) Reset() { *m = Packet{} }
func (m *Packet) String() string { return proto.CompactTextString(m) }
func (*Packet) ProtoMessage() {}
func (m *Packet) GetProof() *Proof {
if m != nil {
return m.Proof
}
return nil
}
func (m *Packet) GetResult() *Result {
if m != nil {
return m.Result
}
return nil
}
type Proof struct {
Proof []byte `protobuf:"bytes,1,opt,name=proof" json:"proof,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Proof) Reset() { *m = Proof{} }
func (m *Proof) String() string { return proto.CompactTextString(m) }
func (*Proof) ProtoMessage() {}
func (m *Proof) GetProof() []byte {
if m != nil {
return m.Proof
}
return nil
}
type Result struct {
Accepted *bool `protobuf:"varint,1,req,name=accepted" json:"accepted,omitempty"`
IsKnownContact *bool `protobuf:"varint,2,opt,name=is_known_contact,json=isKnownContact" json:"is_known_contact,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Result) Reset() { *m = Result{} }
func (m *Result) String() string { return proto.CompactTextString(m) }
func (*Result) ProtoMessage() {}
func (m *Result) GetAccepted() bool {
if m != nil && m.Accepted != nil {
return *m.Accepted
}
return false
}
func (m *Result) GetIsKnownContact() bool {
if m != nil && m.IsKnownContact != nil {
return *m.IsKnownContact
}
return false
}
var E_ClientPublicKey = &proto.ExtensionDesc{
ExtendedType: (*Protocol_Data_Control.OpenChannel)(nil),
ExtensionType: ([]byte)(nil),
Field: 9200,
Name: "Protocol.Data.Auth.TripleEDH.client_public_key",
Tag: "bytes,9200,opt,name=client_public_key,json=clientPublicKey",
Filename: "Auth3EDH.proto",
}
var E_ClientEphmeralPublicKey = &proto.ExtensionDesc{
ExtendedType: (*Protocol_Data_Control.OpenChannel)(nil),
ExtensionType: ([]byte)(nil),
Field: 9300,
Name: "Protocol.Data.Auth.TripleEDH.client_ephmeral_public_key",
Tag: "bytes,9300,opt,name=client_ephmeral_public_key,json=clientEphmeralPublicKey",
Filename: "Auth3EDH.proto",
}
var E_ServerPublicKey = &proto.ExtensionDesc{
ExtendedType: (*Protocol_Data_Control.ChannelResult)(nil),
ExtensionType: ([]byte)(nil),
Field: 9200,
Name: "Protocol.Data.Auth.TripleEDH.server_public_key",
Tag: "bytes,9200,opt,name=server_public_key,json=serverPublicKey",
Filename: "Auth3EDH.proto",
}
var E_ServerEphmeralPublicKey = &proto.ExtensionDesc{
ExtendedType: (*Protocol_Data_Control.ChannelResult)(nil),
ExtensionType: ([]byte)(nil),
Field: 9300,
Name: "Protocol.Data.Auth.TripleEDH.server_ephmeral_public_key",
Tag: "bytes,9300,opt,name=server_ephmeral_public_key,json=serverEphmeralPublicKey",
Filename: "Auth3EDH.proto",
}
func init() {
proto.RegisterType((*Packet)(nil), "Protocol.Data.Auth.TripleEDH.Packet")
proto.RegisterType((*Proof)(nil), "Protocol.Data.Auth.TripleEDH.Proof")
proto.RegisterType((*Result)(nil), "Protocol.Data.Auth.TripleEDH.Result")
proto.RegisterExtension(E_ClientPublicKey)
proto.RegisterExtension(E_ClientEphmeralPublicKey)
proto.RegisterExtension(E_ServerPublicKey)
proto.RegisterExtension(E_ServerEphmeralPublicKey)
}

View File

@ -0,0 +1,28 @@
syntax = "proto2";
package Protocol.Data.Auth.TripleEDH;
import "ControlChannel.proto";
extend Control.OpenChannel {
optional bytes client_public_key = 9200;
optional bytes client_ephmeral_public_key = 9300;
}
extend Control.ChannelResult {
optional bytes server_public_key = 9200;
optional bytes server_ephmeral_public_key = 9300;
}
message Packet {
optional Proof proof = 1;
optional Result result = 2;
}
message Proof {
optional bytes proof = 1; // Encrypted Onion Address
}
message Result {
required bool accepted = 1;
optional bool is_known_contact = 2;
}

View File

@ -0,0 +1,25 @@
package Protocol.Data.AuthHiddenService;
import "ControlChannel.proto";
extend Control.OpenChannel {
optional bytes client_cookie = 7200; // 16 random bytes
}
extend Control.ChannelResult {
optional bytes server_cookie = 7200; // 16 random bytes
}
message Packet {
optional Proof proof = 1;
optional Result result = 2;
}
message Proof {
optional bytes public_key = 1; // DER encoded public key
optional bytes signature = 2; // RSA signature
}
message Result {
required bool accepted = 1;
optional bool is_known_contact = 2;
}

View File

@ -18,7 +18,7 @@ package Protocol_Data_AuthHiddenService
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import Protocol_Data_Control "github.com/s-rah/go-ricochet/control"
import Protocol_Data_Control "git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal

View File

@ -0,0 +1,18 @@
package Protocol.Data.Chat;
message Packet {
optional ChatMessage chat_message = 1;
optional ChatAcknowledge chat_acknowledge = 2;
}
message ChatMessage {
required string message_text = 1;
optional uint32 message_id = 2; // Random ID for ack
optional int64 time_delta = 3; // Delta in seconds between now and when message was written
}
message ChatAcknowledge {
optional uint32 message_id = 1;
optional bool accepted = 2 [default = true];
}

View File

@ -0,0 +1,35 @@
package Protocol.Data.ContactRequest;
import "ControlChannel.proto";
enum Limits {
MessageMaxCharacters = 2000;
NicknameMaxCharacters = 30;
}
extend Control.OpenChannel {
optional ContactRequest contact_request = 200;
}
extend Control.ChannelResult {
optional Response response = 201;
}
// Sent only as an attachment to OpenChannel
message ContactRequest {
optional string nickname = 1;
optional string message_text = 2;
}
// Response is the only valid message to send on the channel
message Response {
enum Status {
Undefined = 0; // Not valid on the wire
Pending = 1;
Accepted = 2;
Rejected = 3;
Error = 4;
}
required Status status = 1;
}

View File

@ -17,7 +17,7 @@ package Protocol_Data_ContactRequest
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import Protocol_Data_Control "github.com/s-rah/go-ricochet/control"
import Protocol_Data_Control "git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal

View File

@ -0,0 +1,52 @@
package Protocol.Data.Control;
message Packet {
// Must contain exactly one field
optional OpenChannel open_channel = 1;
optional ChannelResult channel_result = 2;
optional KeepAlive keep_alive = 3;
optional EnableFeatures enable_features = 4;
optional FeaturesEnabled features_enabled = 5;
}
message OpenChannel {
required int32 channel_identifier = 1; // Arbitrary unique identifier for this channel instance
required string channel_type = 2; // String identifying channel type; e.g. im.ricochet.chat
// It is valid to extend the OpenChannel message to add fields specific
// to the requested channel_type.
extensions 100 to max;
}
message ChannelResult {
required int32 channel_identifier = 1; // Matching the value from OpenChannel
required bool opened = 2; // If the channel is now open
enum CommonError {
GenericError = 0;
UnknownTypeError = 1;
UnauthorizedError = 2;
BadUsageError = 3;
FailedError = 4;
}
optional CommonError common_error = 3;
// As with OpenChannel, it is valid to extend this message with fields specific
// to the channel type.
extensions 100 to max;
}
message KeepAlive {
required bool response_requested = 1;
}
message EnableFeatures {
repeated string feature = 1;
extensions 100 to max;
}
message FeaturesEnabled {
repeated string feature = 1;
extensions 100 to max;
}

View File

@ -130,7 +130,7 @@ func (m *OpenChannel) String() string { return proto.CompactTextString(m) }
func (*OpenChannel) ProtoMessage() {}
var extRange_OpenChannel = []proto.ExtensionRange{
{100, 536870911},
{Start: 100, End: 536870911},
}
func (*OpenChannel) ExtensionRangeArray() []proto.ExtensionRange {
@ -170,7 +170,7 @@ func (m *ChannelResult) String() string { return proto.CompactTextString(m) }
func (*ChannelResult) ProtoMessage() {}
var extRange_ChannelResult = []proto.ExtensionRange{
{100, 536870911},
{Start: 100, End: 536870911},
}
func (*ChannelResult) ExtensionRangeArray() []proto.ExtensionRange {
@ -231,7 +231,7 @@ func (m *EnableFeatures) String() string { return proto.CompactTextString(m) }
func (*EnableFeatures) ProtoMessage() {}
var extRange_EnableFeatures = []proto.ExtensionRange{
{100, 536870911},
{Start: 100, End: 536870911},
}
func (*EnableFeatures) ExtensionRangeArray() []proto.ExtensionRange {
@ -262,7 +262,7 @@ func (m *FeaturesEnabled) String() string { return proto.CompactTextString(m) }
func (*FeaturesEnabled) ProtoMessage() {}
var extRange_FeaturesEnabled = []proto.ExtensionRange{
{100, 536870911},
{Start: 100, End: 536870911},
}
func (*FeaturesEnabled) ExtensionRangeArray() []proto.ExtensionRange {