forked from openprivacy/libricochet-go
Compare commits
186 Commits
Author | SHA1 | Date |
---|---|---|
Sarah Jamie Lewis | f82c2f9da4 | |
Dan Ballard | 7c828d3916 | |
Dan Ballard | 79a1ff9161 | |
Sarah Jamie Lewis | 5a1fc1b94d | |
Dan Ballard | 9ba39b93b7 | |
Sarah Jamie Lewis | 29540dcf71 | |
Dan Ballard | 0fdcfd1553 | |
Dan Ballard | ac4993adb7 | |
Dan Ballard | 881ca5c6c2 | |
Dan Ballard | 61f89d7b2c | |
Sarah Jamie Lewis | 725f64020a | |
Dan Ballard | b7cca3fa83 | |
Sarah Jamie Lewis | 07747c4dd2 | |
Dan Ballard | 59ea2902e8 | |
Sarah Jamie Lewis | 4ccdc79526 | |
Dan Ballard | 6517665498 | |
Sarah Jamie Lewis | cd872e9e0a | |
Dan Ballard | b534ecd04e | |
Sarah Jamie Lewis | aca0f63dd2 | |
Dan Ballard | bf57db657a | |
Sarah Jamie Lewis | 8db3c09fce | |
Dan Ballard | fa12b3dcb9 | |
erinn | 9e4e042ffb | |
Dan Ballard | d78488a200 | |
Sarah Jamie Lewis | b3d6e0e019 | |
Dan Ballard | bf28d6574f | |
Sarah Jamie Lewis | 0739119a4d | |
Sarah Jamie Lewis | 527ba61de0 | |
erinn | af195d186c | |
Sarah Jamie Lewis | 5bfb17588c | |
Dan Ballard | 1131bc930e | |
Sarah Jamie Lewis | 877f01a358 | |
Dan Ballard | 46e3eeeb7d | |
Sarah Jamie Lewis | 96fc03580b | |
Sarah Jamie Lewis | 7a4350f0c1 | |
Sarah Jamie Lewis | b2f6b314fc | |
Sarah Jamie Lewis | 64ce11d436 | |
erinn | 8f00e26b81 | |
Sarah Jamie Lewis | a96f682e77 | |
Dan Ballard | 487b1e9ae0 | |
Sarah Jamie Lewis | 9a680cd257 | |
Dan Ballard | e068de0ef8 | |
Dan Ballard | 1f0e87b3c4 | |
Sarah Jamie Lewis | f4fbd52d4b | |
Sarah Jamie Lewis | d87a0fcb52 | |
Dan Ballard | 859cdf95e1 | |
Dan Ballard | 3137689de8 | |
Sarah Jamie Lewis | 2cf7113f80 | |
Sarah Jamie Lewis | 4c23e439f0 | |
Dan Ballard | 540e68b8ef | |
Sarah Jamie Lewis | b05567fd81 | |
Dan Ballard | 8d1cba2b0b | |
Sarah Jamie Lewis | 90231b0be9 | |
erinn | 328ffc9d76 | |
Dan Ballard | 489bf62ff7 | |
Dan Ballard | b34fe84917 | |
Sarah Jamie Lewis | b6caf3fbc4 | |
Dan Ballard | 2815e29704 | |
Sarah Jamie Lewis | fa720940d8 | |
Dan Ballard | 5697a1c03d | |
Sarah Jamie Lewis | 232849e304 | |
Dan Ballard | 38cff4212d | |
Sarah Jamie Lewis | b8a7cd702a | |
erinn | 65fb3992a3 | |
erinn | 52bbc23251 | |
Dan Ballard | 53e6f90925 | |
Dan Ballard | b1fb04a880 | |
Dan Ballard | 0d080e4332 | |
Dan Ballard | 4da220b223 | |
Sarah Jamie Lewis | d54ed0b106 | |
Dan Ballard | 52ce7615c3 | |
Sarah Jamie Lewis | aba5fa4609 | |
Dan Ballard | 6c0d17667b | |
Sarah Jamie Lewis | 69e6644eb2 | |
Sarah Jamie Lewis | 353ef38a54 | |
Dan Ballard | 8fc60a0495 | |
Dan Ballard | 880bd2e020 | |
Dan Ballard | 493c2f5ec0 | |
Sarah Jamie Lewis | 18bc23d5c1 | |
Dan Ballard | dfba540973 | |
Dan Ballard | 3900552e06 | |
Sarah Jamie Lewis | 49faf95ee7 | |
erinn | 4d3f52102f | |
erinn | ad9d0efb02 | |
Dan Ballard | 5b914aaf3a | |
erinn | 1abbb874cc | |
erinn | 8960be643c | |
erinn | 1056fce116 | |
Dan Ballard | 29e3a42cc9 | |
Sarah Jamie Lewis | bce6496829 | |
erinn | e825e52a7c | |
erinn | fb8c0cac27 | |
erinn | 5c98fd575b | |
Sarah Jamie Lewis | dd2af9f059 | |
erinn | 8a7895a359 | |
erinn | fd01dca056 | |
Sarah Jamie Lewis | 3ffe8ad0e4 | |
erinn | aa7b398646 | |
Sarah Jamie Lewis | 6c37867ed1 | |
erinn | a6ca1571ad | |
erinn | 06381579e5 | |
Dan Ballard | 530fa1f39f | |
Sarah Jamie Lewis | 5066380655 | |
Dan Ballard | 1667868d8c | |
Sarah Jamie Lewis | 18dcf5c2ae | |
Dan Ballard | 90e39869a9 | |
Sarah Jamie Lewis | 930b94e0be | |
Dan Ballard | b3c09e5409 | |
Sarah Jamie Lewis | cc6076760f | |
Dan Ballard | cdb458647f | |
Sarah Jamie Lewis | 69c1dc18fb | |
Dan Ballard | e0411ecb9a | |
Sarah Jamie Lewis | c1907d5a9f | |
Dan Ballard | a852bc6678 | |
Sarah Jamie Lewis | 2dc3739f56 | |
Dan Ballard | 4442ab6691 | |
Dan Ballard | 7a1c1eb97e | |
Sarah Jamie Lewis | e382c8eb69 | |
Dan Ballard | 5a94afa0f7 | |
Sarah Jamie Lewis | 417d25dc7c | |
Sarah Jamie Lewis | 6f9718596d | |
Sarah Jamie Lewis | 9980da3bd5 | |
Dan Ballard | 3920d33a77 | |
Sarah Jamie Lewis | 92b9a0eb1f | |
Sarah Jamie Lewis | 71685b9c3a | |
Sarah Jamie Lewis | 9191b7530e | |
Sarah Jamie Lewis | 1e33c17ae3 | |
Sarah Jamie Lewis | 4994e54025 | |
Sarah Jamie Lewis | f9209e187c | |
Sarah Jamie Lewis | a04b3fe08b | |
Sarah Jamie Lewis | cf46554922 | |
Sarah Jamie Lewis | 5d9f2ce9e3 | |
Sarah Jamie Lewis | 88d32191f7 | |
Sarah Jamie Lewis | b378c4c825 | |
Sarah Jamie Lewis | 9788c07ac4 | |
Sarah Jamie Lewis | bf19d1b20c | |
Sarah Jamie Lewis | 39cf4d3871 | |
Sarah Jamie Lewis | 339995c101 | |
Sarah Jamie Lewis | 4ccf95bee0 | |
Sarah Jamie Lewis | 30808c71b2 | |
Sarah Jamie Lewis | 6e2bfbbc14 | |
Sarah Jamie Lewis | 4b700d4223 | |
Sarah Jamie Lewis | 1a2fb40d91 | |
Sarah Jamie Lewis | be62634c46 | |
Sarah Jamie Lewis | 05e8675ed5 | |
Sarah Jamie Lewis | f6cc472c6e | |
Sarah Jamie Lewis | 617ca019f8 | |
Sarah Jamie Lewis | 7f215e86c4 | |
Sarah Jamie Lewis | 84d7336602 | |
Sarah Jamie Lewis | 049a0ea15f | |
Sarah Jamie Lewis | 6d449e230f | |
Sarah Jamie Lewis | f537fb4f76 | |
Sarah Jamie Lewis | 1433b31e6f | |
Sarah Jamie Lewis | 43b357fdb6 | |
Sarah Jamie Lewis | e1031861a2 | |
Sarah Jamie Lewis | 958e07bf66 | |
Sarah Jamie Lewis | 5057dd68ee | |
Sarah Jamie Lewis | 9d2e898157 | |
Sarah Jamie Lewis | 3e6dc80670 | |
Sarah Jamie Lewis | dc285b18a9 | |
Sarah Jamie Lewis | 8fe7b84fc9 | |
John Brooks | 9a65aeed77 | |
John Brooks | b2c87b1b72 | |
John Brooks | e459a56286 | |
John Brooks | c24773809e | |
John Brooks | 0f47f62465 | |
John Brooks | d19102b257 | |
Sarah Jamie Lewis | 1ed9265866 | |
Sarah Jamie Lewis | ec16eee2aa | |
John Brooks | a62d1bbcc9 | |
John Brooks | ea788d58ef | |
John Brooks | 41d9401ca4 | |
Dan Ballard | f04239c885 | |
Sarah Jamie Lewis | d2dceef028 | |
Dan Ballard | 5937ceee73 | |
Sarah Jamie Lewis | 93baafc2f7 | |
Sarah Jamie Lewis | 22cbf5d738 | |
Sarah Jamie Lewis | 1cf7c2b7c7 | |
Sarah Jamie Lewis | f4ed1c244b | |
Sarah Jamie Lewis | a2fa40492a | |
Sarah Jamie Lewis | 4f1a2f82cc | |
Dan Ballard | d895b46a03 | |
Dan Ballard | 6f07cff0bc | |
Sarah Jamie Lewis | 5d767174b1 | |
Sarah Jamie Lewis | 5a720a08d0 | |
John Brooks | 860ae9a024 |
|
@ -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
|
|
@ -1,3 +1,9 @@
|
|||
go-ricochet-coverage.out
|
||||
*~
|
||||
*.out
|
||||
.idea
|
||||
.reviewboardrc
|
||||
/vendor/
|
||||
/testing/tor/
|
||||
/connectivity/tor/
|
||||
/tor/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
12
LICENSE
|
@ -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.
|
||||
|
|
63
README.md
63
README.md
|
@ -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).
|
||||
|
|
|
@ -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() {
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// +build !windows
|
||||
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var sysProcAttr = &syscall.SysProcAttr{}
|
|
@ -0,0 +1,9 @@
|
|||
// +build windows
|
||||
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var sysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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...)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package goricochet
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOpenConnectionAuth(t *testing.T) {
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
420
ricochet.go
420
ricochet.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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 < 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`
|
||||
|
|
@ -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!")
|
||||
}
|
||||
}
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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
|
|
@ -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
|
||||
|
9
tests.sh
9
tests.sh
|
@ -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
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
58
utils/tor.go
58
utils/tor.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
Loading…
Reference in New Issue