From bfe5b7436415dc811ccc6f90e6beb045382ee2db Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 26 Jun 2016 18:56:23 -0700 Subject: [PATCH] Refactor GoRicochet * New Service Interface * Server functionality * 90% Code Coverage * Regression Testing of Protocol Compliance --- .gitignore | 1 + .travis.yml | 2 +- CONTRIBUTING.md | 51 ++ LICENSE | 51 +- Makefile | 9 + README.md | 68 ++- authhandler.go | 2 +- authhandler_test.go | 4 +- examples/echobot/main.go | 37 +- logo.png | Bin 0 -> 12099 bytes messagebuilder.go | 120 +++- messagebuilder_test.go | 4 +- messagedecoder.go | 110 ---- openconnection.go | 300 +++++++++ openconnection_test.go | 7 + private_key | 15 + ricochet.go | 569 ++++++++++-------- ricochetservice.go | 37 +- standardricochetservice.go | 165 +++-- ...ardricochetservice_bad_usage_error_test.go | 108 ++++ standardricochetservice_test.go | 107 ++++ standardricochetservice_unauth_test.go | 63 ++ ...ardricochetservice_unknown_contact_test.go | 60 ++ utils/error.go | 19 + utils/networking.go | 92 +++ utils/networking_test.go | 171 ++++++ .../networkresolver.go | 3 +- utils/tor.go | 19 + utils/tor_test.go | 43 ++ 29 files changed, 1773 insertions(+), 464 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 logo.png delete mode 100644 messagedecoder.go create mode 100644 openconnection.go create mode 100644 openconnection_test.go create mode 100644 private_key create mode 100644 standardricochetservice_bad_usage_error_test.go create mode 100644 standardricochetservice_test.go create mode 100644 standardricochetservice_unauth_test.go create mode 100644 standardricochetservice_unknown_contact_test.go create mode 100644 utils/error.go create mode 100644 utils/networking.go create mode 100644 utils/networking_test.go rename networkresolver.go => utils/networkresolver.go (95%) create mode 100644 utils/tor.go create mode 100644 utils/tor_test.go diff --git a/.gitignore b/.gitignore index 77c8c94..5c9955e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ go-ricochet-coverage.out +*~ diff --git a/.travis.yml b/.travis.yml index 53e91a1..f166263 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: go -script: go get github.com/golang/protobuf/proto && go get h12.me/socks && go test -cover +script: go get github.com/golang/protobuf/proto && go get h12.me/socks && go test -v -cover diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1b709df --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# How To Contribute to GoRicochet + +This document highlights some useful tips for contributing to this project. Feel +free to submit pull requests to update this document as needed. + +# Requesting a New Feature + +You can request a new feature by submitting an issue to the [Github Repository](https://github.com/s-rah/go-ricochet). + +# Writing New Code + +So you want to dig in? Awesome! Here are a few steps to consider: + +## 1. Before working on a Change + +First, check to see if your proposed change is already in active development. This +can be done by searching issues and pull requests on Github. + +If no issue exists for your change then please open one. You **do not** need to wait +for the maintainers or the community to discuss your change before starting work but, for +larger changes, we suggest attaching some design notes and requesting feedback from the +maintainers to avoid the change being rejected after all that hard work! + +## 2. Make the Change + +Crack open the editor of your choice and start programming. As you go along ensure +to run `go test github.com/s-rah/go-ricochet` to ensure that everything is working! + +Please write tests for any new functionality. As a rule, aim for >80% code coverage. You +can check coverage `with go test --cover github.com/s-rah/go-ricochet` + +## 3. Before Submitting a Pull Request + +Format your code (the path might be slightly different): + +* `gofmt -l=true -s -w src/github.com/s-rah/go-ricochet/` + +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` + +## 4. Code Review + +Once you submit the pull request it will be reviewed by one of the maintainers +of the project. At this point there are 3 possible paths: + +1. Your change is accepted! +2. Your change is acknowledged as necessary, but requires some rework before being accepted. +3. Your change is rejected - this can happen for a number of reasons. To minimize the chances of this happening, please see step #1 diff --git a/LICENSE b/LICENSE index f18ad0d..51326fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,9 @@ +All code in this project, unless explicitly stated elsewhere in this document is +covered under the following license. + The MIT License (MIT) -Copyright (c) 2015 Sarah Jamie Lewis +Copyright (c) 2016 Sarah Jamie Lewis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +22,49 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +Autogenerated protobuf code was generated using the proto file from Ricochet. +They are covered under the following license. + + Ricochet - https://ricochet.im/ +Copyright (C) 2014, John Brooks + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + + * Neither the names of the copyright owners nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The go-ricochet logo is based on an image by Olga Shalakhina + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b349943 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +all: + go install github.com/go-ricochet + +test: + go test -v github.com/s-rah/go-ricochet/... + +cover: + go test github.com/s-rah/go-ricochet/... -cover + diff --git a/README.md b/README.md index f6a0aca..3c87ec6 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,63 @@ # GoRicochet [![Build Status](https://travis-ci.org/s-rah/go-ricochet.svg?branch=master)](https://travis-ci.org/s-rah/go-ricochet) -GoRicochet is an implementation of the [Ricochet Protocol](https://ricochet.im) +![GoRicochet](logo.png) + +GoRicochet is an experimental implementation of the [Ricochet Protocol](https://ricochet.im) in Go. -**NOTE:** This project is in the very early stages and is in no way meant to be -a replacement for the core Ricochet implementation. This version exists for -the purpose of writing testing tools for Ricochet in Go. +## Features -## Current Features +* A simple API that you can use to build Automated Ricochet Applications +* A suite of regression tests that test protocol compliance. -* Connect to a Local Ricochet Client -* Issue an Authentication Request -* Issue a Contact Request -* Open a new Channel -* Send a Chat Message +## Building an Automated Ricochet Application -If you have questions or want to contribute please contact Sarah @ -`ricochet:qn6uo4cmsrfv4kzq` +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) + +## 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). diff --git a/authhandler.go b/authhandler.go index 1291120..f2890ef 100644 --- a/authhandler.go +++ b/authhandler.go @@ -7,7 +7,7 @@ import ( "io" ) -// AuthenticationHandler manages the stae required for the AuthHiddenService +// AuthenticationHandler manages the state required for the AuthHiddenService // authentication scheme for ricochet. type AuthenticationHandler struct { clientCookie [16]byte diff --git a/authhandler_test.go b/authhandler_test.go index 1b919a9..bfcdca1 100644 --- a/authhandler_test.go +++ b/authhandler_test.go @@ -19,7 +19,7 @@ func TestGenClientCookie(t *testing.T) { authHandler := new(AuthenticationHandler) clientCookie := authHandler.GenClientCookie() if clientCookie != authHandler.clientCookie { - t.Errorf("AuthenticationHandler Client Cookies are Different", clientCookie, authHandler.clientCookie) + t.Errorf("AuthenticationHandler Client Cookies are Different %x %x", clientCookie, authHandler.clientCookie) } } @@ -27,6 +27,6 @@ func TestGenServerCookie(t *testing.T) { authHandler := new(AuthenticationHandler) serverCookie := authHandler.GenServerCookie() if serverCookie != authHandler.serverCookie { - t.Errorf("AuthenticationHandler Server Cookies are Different", serverCookie, authHandler.serverCookie) + t.Errorf("AuthenticationHandler Server Cookies are Different %x %x", serverCookie, authHandler.serverCookie) } } diff --git a/examples/echobot/main.go b/examples/echobot/main.go index 7ce0de9..6234d90 100644 --- a/examples/echobot/main.go +++ b/examples/echobot/main.go @@ -2,30 +2,35 @@ package main import ( "github.com/s-rah/go-ricochet" + "log" ) type EchoBotService struct { - goricochet.StandardRicochetService + goricochet.StandardRicochetService } -func (ebs * EchoBotService) OnAuthenticationResult(channelID int32, serverHostname string, result bool) { - if true { - ebs.Ricochet().OpenChatChannel(5) - ebs.Ricochet().SendMessage(5, "Hi I'm an echo bot, I echo what you say!") - } +// Always Accept Contact Requests +func (ts *EchoBotService) IsKnownContact(hostname string) bool { + return true } -func (ebs * EchoBotService) OnChatMessage(channelID int32, serverHostname string, messageId int32, message string) { - ebs.Ricochet().AckChatMessage(channelID, messageId) - ebs.Ricochet().SendMessage(5, message) +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", "kwke2hntvyfqm7dr") - err := ricochetService.Ricochet().Connect("kwke2hntvyfqm7dr", "127.0.0.1:55555|jlq67qzo6s4yp3sp") - if err == nil { - ricochetService.OnConnect("jlq67qzo6s4yp3sp") - ricochetService.Ricochet().ListenAndWait("jlq67qzo6s4yp3sp", ricochetService) - } + ricochetService := new(EchoBotService) + ricochetService.Init("./private_key") + ricochetService.Listen(ricochetService, 12345) } diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..16b01c5d2517142149567b92fac72ba91229c546 GIT binary patch literal 12099 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 Hu#fs5!psTW literal 0 HcmV?d00001 diff --git a/messagebuilder.go b/messagebuilder.go index 7c9eb33..874f213 100644 --- a/messagebuilder.go +++ b/messagebuilder.go @@ -6,6 +6,7 @@ import ( "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 @@ -13,12 +14,12 @@ import ( type MessageBuilder struct { } -// OpenChatChannel contructs a message which will request to open a channel for +// OpenChannel contructs a message which will request to open a channel for // chat on the given channelID. -func (mb *MessageBuilder) OpenChatChannel(channelID int32) ([]byte, error) { +func (mb *MessageBuilder) OpenChannel(channelID int32, channelType string) ([]byte, error) { oc := &Protocol_Data_Control.OpenChannel{ ChannelIdentifier: proto.Int32(channelID), - ChannelType: proto.String("im.ricochet.chat"), + ChannelType: proto.String(channelType), } pc := &Protocol_Data_Control.Packet{ OpenChannel: oc, @@ -27,10 +28,10 @@ func (mb *MessageBuilder) OpenChatChannel(channelID int32) ([]byte, error) { } // AckOpenChannel constructs a message to acknowledge a previous open channel operation. -func (mb *MessageBuilder) AckOpenChannel(channelID int32, opened bool) ([]byte, error) { +func (mb *MessageBuilder) AckOpenChannel(channelID int32) ([]byte, error) { cr := &Protocol_Data_Control.ChannelResult{ ChannelIdentifier: proto.Int32(channelID), - Opened: proto.Bool(opened), + Opened: proto.Bool(true), } pc := &Protocol_Data_Control.Packet{ ChannelResult: cr, @@ -38,6 +39,39 @@ func (mb *MessageBuilder) AckOpenChannel(channelID int32, opened bool) ([]byte, return proto.Marshal(pc) } +// 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) { + + errorNum := Protocol_Data_Control.ChannelResult_CommonError_value[error] + commonError := Protocol_Data_Control.ChannelResult_CommonError(errorNum) + + cr := &Protocol_Data_Control.ChannelResult{ + ChannelIdentifier: proto.Int32(channelID), + Opened: proto.Bool(false), + CommonError: &commonError, + } + pc := &Protocol_Data_Control.Packet{ + ChannelResult: cr, + } + return proto.Marshal(pc) +} + +// ConfirmAuthChannel constructs a message to acknowledge a previous open channel operation. +func (mb *MessageBuilder) ConfirmAuthChannel(channelID int32, serverCookie [16]byte) ([]byte, error) { + 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) + + pc := &Protocol_Data_Control.Packet{ + ChannelResult: cr, + } + return proto.Marshal(pc) +} + // 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) { @@ -53,10 +87,7 @@ func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string } err := proto.SetExtension(oc, Protocol_Data_ContactRequest.E_ContactRequest, contactRequest) - - if err != nil { - return nil, err - } + utils.CheckError(err) pc := &Protocol_Data_Control.Packet{ OpenChannel: oc, @@ -64,6 +95,38 @@ func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string return proto.Marshal(pc) } +// ReplyToContactRequestOnResponse constructs a message to acknowledge contact request +func (mb *MessageBuilder) ReplyToContactRequestOnResponse(channelID int32, status string) ([]byte, error) { + cr := &Protocol_Data_Control.ChannelResult{ + ChannelIdentifier: proto.Int32(channelID), + Opened: proto.Bool(true), + } + + statusNum := Protocol_Data_ContactRequest.Response_Status_value[status] + responseStatus := Protocol_Data_ContactRequest.Response_Status(statusNum) + contactRequest := &Protocol_Data_ContactRequest.Response{ + Status: &responseStatus, + } + + err := proto.SetExtension(cr, Protocol_Data_ContactRequest.E_Response, contactRequest) + utils.CheckError(err) + + pc := &Protocol_Data_Control.Packet{ + ChannelResult: cr, + } + return proto.Marshal(pc) +} + +// ReplyToContactRequest constructs a message to acknowledge a contact request +func (mb *MessageBuilder) ReplyToContactRequest(channelID int32, status string) ([]byte, error) { + 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) +} + // 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) { @@ -72,18 +135,49 @@ func (mb *MessageBuilder) OpenAuthenticationChannel(channelID int32, clientCooki ChannelType: proto.String("im.ricochet.auth.hidden-service"), } err := proto.SetExtension(oc, Protocol_Data_AuthHiddenService.E_ClientCookie, clientCookie[:]) - if err != nil { - return nil, err - } + utils.CheckError(err) + pc := &Protocol_Data_Control.Packet{ OpenChannel: oc, } return proto.Marshal(pc) } +// Proof constructs a proof message with the given public key and signature. +func (mb *MessageBuilder) Proof(publicKeyBytes []byte, signatureBytes []byte) ([]byte, error) { + proof := &Protocol_Data_AuthHiddenService.Proof{ + PublicKey: publicKeyBytes, + Signature: signatureBytes, + } + + ahsPacket := &Protocol_Data_AuthHiddenService.Packet{ + Proof: proof, + Result: nil, + } + + return proto.Marshal(ahsPacket) +} + +// AuthResult constructs a response to a Proof +func (mb *MessageBuilder) AuthResult(accepted bool, isKnownContact bool) ([]byte, error) { + // Construct a Result Message + result := &Protocol_Data_AuthHiddenService.Result{ + Accepted: proto.Bool(accepted), + IsKnownContact: proto.Bool(isKnownContact), + } + + ahsPacket := &Protocol_Data_AuthHiddenService.Packet{ + Proof: nil, + Result: result, + } + + return proto.Marshal(ahsPacket) +} + // ChatMessage constructs a chat message with the given content. -func (mb *MessageBuilder) ChatMessage(message string) ([]byte, error) { +func (mb *MessageBuilder) ChatMessage(message string, messageID int32) ([]byte, error) { cm := &Protocol_Data_Chat.ChatMessage{ + MessageId: proto.Uint32(uint32(messageID)), MessageText: proto.String(message), } chatPacket := &Protocol_Data_Chat.Packet{ diff --git a/messagebuilder_test.go b/messagebuilder_test.go index 1d5ace9..e0083de 100644 --- a/messagebuilder_test.go +++ b/messagebuilder_test.go @@ -4,7 +4,7 @@ import "testing" func TestOpenChatChannel(t *testing.T) { messageBuilder := new(MessageBuilder) - _, err := messageBuilder.OpenChatChannel(1) + _, err := messageBuilder.OpenChannel(1, "im.ricochet.chat") if err != nil { t.Errorf("Error building open chat channel message: %s", err) } @@ -31,7 +31,7 @@ func TestOpenAuthenticationChannel(t *testing.T) { func TestChatMessage(t *testing.T) { messageBuilder := new(MessageBuilder) - _, err := messageBuilder.ChatMessage("Hello World") + _, err := messageBuilder.ChatMessage("Hello World", 0) if err != nil { t.Errorf("Error building chat message: %s", err) } diff --git a/messagedecoder.go b/messagedecoder.go deleted file mode 100644 index 48f758b..0000000 --- a/messagedecoder.go +++ /dev/null @@ -1,110 +0,0 @@ -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/control" -) - -type MessageDecoder struct { -} - -// Conceptual Chat Message - we construct this to avoid polluting the -// the main ricochet code with protobuf cruft - and enable us to minimise the -// code that may break in the future. -type RicochetChatMessage struct { - Ack bool - MessageID int32 - Message string - Accepted bool -} - -// Conceptual Control Message - we construct this to avoid polluting the -// the main ricochet code with protobuf cruft - and enable us to minimise the -// code that may break in the future. -type RicochetControlMessage struct { - Ack bool - Type string - ChannelID int32 - Accepted bool - ClientCookie [16]byte - ServerCookie [16]byte -} - -// DecodeAuthMessage -func (md *MessageDecoder) DecodeAuthMessage(data []byte) (bool, error) { - res := new(Protocol_Data_AuthHiddenService.Packet) - err := proto.Unmarshal(data[:], res) - if err != nil { - return false, errors.New("error unmarshalling control message type") - } - return res.GetResult().GetAccepted(), nil -} - -// DecodeControlMessage -func (md *MessageDecoder) DecodeControlMessage(data []byte) (*RicochetControlMessage, error) { - res := new(Protocol_Data_Control.Packet) - err := proto.Unmarshal(data[:], res) - - if err != nil { - return nil, errors.New("error unmarshalling control message type") - } - - if res.GetOpenChannel() != nil { - ricochetControlMessage := new(RicochetControlMessage) - ricochetControlMessage.Ack = false - - if res.GetOpenChannel().GetChannelType() == "im.ricochet.auth.hidden-service" { - ricochetControlMessage.Type = "openauthchannel" - } - - ricochetControlMessage.Type = "openchannel" - ricochetControlMessage.ChannelID = int32(res.GetOpenChannel().GetChannelIdentifier()) - return ricochetControlMessage, nil - } else if res.GetChannelResult() != nil { - ricochetControlMessage := new(RicochetControlMessage) - ricochetControlMessage.Ack = true - ricochetControlMessage.ChannelID = int32(res.GetOpenChannel().GetChannelIdentifier()) - - serverCookie, err := proto.GetExtension(res.GetChannelResult(), Protocol_Data_AuthHiddenService.E_ServerCookie) - - if err == nil { - ricochetControlMessage.Type = "openauthchannel" - copy(ricochetControlMessage.ServerCookie[:], serverCookie.([]byte)) - } else { - ricochetControlMessage.Type = "openchannel" - - } - - return ricochetControlMessage, nil - } - return nil, errors.New("unknown control message type") -} - -// DecodeChatMessage takes a byte representing a data packet and returns a -// constructed RicochetControlMessage -func (md *MessageDecoder) DecodeChatMessage(data []byte) (*RicochetChatMessage, error) { - res := new(Protocol_Data_Chat.Packet) - err := proto.Unmarshal(data[:], res) - - if err != nil { - return nil, err - } - - if res.GetChatMessage() != nil { - ricochetChatMessage := new(RicochetChatMessage) - ricochetChatMessage.Ack = false - ricochetChatMessage.MessageID = int32(res.GetChatMessage().GetMessageId()) - ricochetChatMessage.Message = res.GetChatMessage().GetMessageText() - return ricochetChatMessage, nil - } else if res.GetChatAcknowledge != nil { - ricochetChatMessage := new(RicochetChatMessage) - ricochetChatMessage.Ack = true - ricochetChatMessage.MessageID = int32(res.GetChatAcknowledge().GetMessageId()) - ricochetChatMessage.Accepted = res.GetChatAcknowledge().GetAccepted() - return ricochetChatMessage, nil - } - return nil, errors.New("chat message type not supported") -} diff --git a/openconnection.go b/openconnection.go new file mode 100644 index 0000000..3138f8a --- /dev/null +++ b/openconnection.go @@ -0,0 +1,300 @@ +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 intializes 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 { + 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) +} diff --git a/openconnection_test.go b/openconnection_test.go new file mode 100644 index 0000000..9487e16 --- /dev/null +++ b/openconnection_test.go @@ -0,0 +1,7 @@ +package goricochet + +import "testing" + +func TestOpenConnectionAuth(t *testing.T) { + +} diff --git a/private_key b/private_key new file mode 100644 index 0000000..40b9757 --- /dev/null +++ b/private_key @@ -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----- diff --git a/ricochet.go b/ricochet.go index 5e785ca..a814b1c 100644 --- a/ricochet.go +++ b/ricochet.go @@ -1,319 +1,372 @@ package goricochet import ( - "encoding/binary" "errors" - "fmt" "github.com/golang/protobuf/proto" "github.com/s-rah/go-ricochet/auth" - "io/ioutil" + "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" "log" "net" - "os" + "strconv" ) // Ricochet is a protocol to conducting anonymous IM. type Ricochet struct { - conn net.Conn - logger *log.Logger -} - -// RicochetData is a structure containing the raw data and the channel it the -// message originated on. -type RicochetData struct { - Channel int32 - Data []byte + newconns chan *OpenConnection + networkResolver utils.NetworkResolver + rni utils.RicochetNetworkInterface } // Init sets up the Ricochet object. -func (r *Ricochet) Init(debugLog bool) { - - if debugLog { - r.logger = log.New(os.Stdout, "[Ricochet]: ", log.Ltime|log.Lmicroseconds) - } else { - r.logger = log.New(ioutil.Discard, "[Ricochet]: ", log.Ltime|log.Lmicroseconds) - } +func (r *Ricochet) Init() { + r.newconns = make(chan *OpenConnection) + r.networkResolver = utils.NetworkResolver{} + r.rni = new(utils.RicochetNetwork) } -// Connect sets up a ricochet connection between from and to which are -// both ricochet formated hostnames e.g. qn6uo4cmsrfv4kzq.onion. If this +// 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(from string, to string) error { +func (r *Ricochet) Connect(host string) (*OpenConnection, error) { var err error - networkResolver := new(NetworkResolver) - r.conn, to, err = networkResolver.Resolve(to) + conn, host, err := r.networkResolver.Resolve(host) if err != nil { - return err + return nil, err } - return r.negotiateVersion() -} - -// Authenticate opens an Authentication Channel and send a client cookie -func (r *Ricochet) Authenticate(channelID int32, clientCookie [16]byte) error { - messageBuilder := new(MessageBuilder) - data, err := messageBuilder.OpenAuthenticationChannel(channelID, clientCookie) - + oc, err := r.negotiateVersion(conn, true) if err != nil { - return errors.New("Cannot Marshal Open Channel Message") + return nil, err } - r.logger.Printf("Sending Open Channel with Auth Request (channel:%d)", channelID) - r.sendPacket(data, 0) - return nil + oc.OtherHostname = host + r.newconns <- oc + return oc, nil } -// SendProof sends an authentication proof in response to a challenge. -func (r *Ricochet) SendProof(channelID int32, publickeyBytes []byte, signatureBytes []byte) error { - // Construct a Proof Message - proof := &Protocol_Data_AuthHiddenService.Proof{ - PublicKey: publickeyBytes, - Signature: signatureBytes, - } - - ahsPacket := &Protocol_Data_AuthHiddenService.Packet{ - Proof: proof, - Result: nil, - } - - data, err := proto.Marshal(ahsPacket) - +// 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 { - return err + log.Printf("Cannot Listen on Port %v", port) + return } - r.logger.Printf("Sending Proof Auth Request (channel:%d)", channelID) - r.sendPacket(data, channelID) - return nil -} - -// OpenChannel opens a new chat channel with the given id -// Prerequisites: -// * Must have Previously issued a successful Connect() -// * If acting as the client, id must be odd, else even -func (r *Ricochet) OpenChatChannel(id int32) error { - messageBuilder := new(MessageBuilder) - data, err := messageBuilder.OpenChatChannel(id) - - if err != nil { - return errors.New("error constructing control channel message to open channel") + go r.ProcessMessages(service) + service.OnReady() + for { + // accept connection on port + conn, err := ln.Accept() + if err != nil { + return + } + go r.processNewConnection(conn, service) } - - r.logger.Printf("Opening Chat Channel: %d", id) - r.sendPacket(data, 0) - return nil } -// SendContactRequest initiates a contact request to the server. -// Prerequisites: -// * Must have Previously issued a successful Connect() -func (r *Ricochet) SendContactRequest(channel int32, nick string, message string) error { - messageBuilder := new(MessageBuilder) - data, err := messageBuilder.OpenContactRequestChannel(channel, nick, message) - - if err != nil { - return errors.New("error constructing control channel message to send contact request") +// 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) } - - r.sendPacket(data, 0) - return nil } -// AckOpenChannel acknowledges a previously received open channel message -// Prerequisites: -// * Must have Previously issued a successful Connect() -func (r *Ricochet) AckOpenChannel(channel int32, result bool) error { - messageBuilder := new(MessageBuilder) - data, err := messageBuilder.AckOpenChannel(channel, result) - if err != nil { - return errors.New("Failed to serialize open channel ack") - } - r.sendPacket(data, 0) - return nil -} - -// AckChatMessage acknowledges a previously received chat message. -// Prerequisites: -// * Must have Previously issued a successful Connect() -func (r *Ricochet) AckChatMessage(channel int32, messageID int32) error { - messageBuilder := new(MessageBuilder) - data, err := messageBuilder.AckChatMessage(messageID) - if err != nil { - return errors.New("Failed to serialize chat message ack") - } - r.sendPacket(data, channel) - return nil -} - -// SendMessage sends a Chat Message (message) to a give Channel (channel). +// 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() -// * Must have previously opened channel with OpenChanel -func (r *Ricochet) SendMessage(channel int32, message string) error { - messageBuilder := new(MessageBuilder) - data, err := messageBuilder.ChatMessage(message) - - if err != nil { - return errors.New("error constructing control channel message to send chat message") +func (r *Ricochet) ProcessMessages(service RicochetService) { + for { + oc := <-r.newconns + go r.processConnection(oc, service) } +} - r.logger.Printf("Sending Message on Channel: %d", channel) - r.sendPacket(data, channel) - return 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) + for { + if oc.Closed { + return + } + + packets, err := r.rni.RecvRicochetPackets(oc.conn) + + if err != nil { + return + } + + for _, packet := range packets { + + 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() + } + } + } } // negotiateVersion Perform version negotiation with the connected host. -func (r *Ricochet) negotiateVersion() error { +func (r *Ricochet) negotiateVersion(conn net.Conn, outbound bool) (*OpenConnection, error) { version := make([]byte, 4) version[0] = 0x49 version[1] = 0x4D version[2] = 0x01 version[3] = 0x01 - fmt.Fprintf(r.conn, "%s", version) - r.logger.Print("Negotiating Version ", version) - res, err := r.recv() + // If this was initiated by us then we need to initiate the version info. + if outbound { + // Send Version String - if len(res) != 1 || err != nil { - return errors.New("Failed Version Negotiating") - } + conn.Write(version) + res, err := r.rni.Recv(conn) - if res[0] != 1 { - return errors.New("Failed Version Negotiating - Invalid Version ") - } + if len(res) != 1 || err != nil { + return nil, errors.New("Failed Version Negotiating") + } - r.logger.Print("Successfully Negotiated Version ", res[0]) - return nil -} + if res[0] != 1 { + return nil, errors.New("Failed Version Negotiating - Invalid Version ") + } + } else { + // Do Version Negotiation -// sendPacket places the data into a structure needed for the client to -// decode the packet and writes the packet to the network. -func (r *Ricochet) sendPacket(data []byte, channel int32) { - header := make([]byte, 4+len(data)) - header[0] = byte(len(header) >> 8) - header[1] = byte(len(header) & 0x00FF) - header[2] = 0x00 - header[3] = byte(channel) - copy(header[4:], data[:]) - fmt.Fprintf(r.conn, "%s", header) -} + buf := make([]byte, 10) + n, err := conn.Read(buf) + if err != nil && n >= 4 { + return nil, err + } -// ListenAndWait is intended to be a background thread listening for all messages -// a client will send, automaticall responding to some, and making the others available to -// Listen() -// Prerequisites: -// * Must have previously issued a successful Connect() -func (r *Ricochet) ListenAndWait(serverHostname string, service RicochetService) error { - for true { - packets, err := r.getMessages() - r.handleFatal(err, "Error attempted to get new messages") - - messageDecoder := new(MessageDecoder) - - for _, packet := range packets { - - if len(packet.Data) == 0 { - r.logger.Printf("Closing Channel %d", packet.Channel) - service.OnChannelClose(packet.Channel, serverHostname) - break - } - - if packet.Channel == 0 { - - message, err := messageDecoder.DecodeControlMessage(packet.Data) - - if err != nil { - r.logger.Printf("Failed to decode data packet, discarding") - break - } - - if message.Type == "openchannel" && message.Ack == false { - r.logger.Printf("new open channel request %d %s", message.ChannelID, serverHostname) - service.OnOpenChannelRequest(message.ChannelID, serverHostname) - } else if message.Type == "openchannel" && message.Ack == true { - r.logger.Printf("new open channel request ack %d %s", message.ChannelID, serverHostname) - service.OnOpenChannelRequestAck(message.ChannelID, serverHostname, message.Accepted) - } else if message.Type == "openauthchannel" && message.Ack == true { - r.logger.Printf("new authentication challenge %d %s", message.ChannelID, serverHostname) - service.OnAuthenticationChallenge(message.ChannelID, serverHostname, message.ServerCookie) - } else { - r.logger.Printf("Received Unknown Control Message\n", message) - } - } else if packet.Channel == 1 { - result, _ := messageDecoder.DecodeAuthMessage(packet.Data) - r.logger.Printf("newreceived auth result %d", packet.Channel) - service.OnAuthenticationResult(1, serverHostname, result) - } else { - - // At this point the only other expected type of message is a Chat Message - messageDecoder := new(MessageDecoder) - message, err := messageDecoder.DecodeChatMessage(packet.Data) - if err != nil { - r.logger.Printf("Failed to decode data packet, discarding on channel %d", packet.Channel) - break - } - - if message.Ack == true { - service.OnChatMessageAck(packet.Channel, serverHostname, message.MessageID) - } else { - service.OnChatMessage(packet.Channel, serverHostname, message.MessageID, message.Message) + if buf[0] == version[0] && buf[1] == version[1] { + foundVersion := false + if buf[2] >= 1 { + for i := 3; i < n; i++ { + if buf[i] == 0x01 { + conn.Write([]byte{0x01}) + foundVersion = true + } } } + if !foundVersion { + return nil, errors.New("Failed Version Negotiating - No Available Version") + } + } else { + return nil, errors.New("Failed Version Negotiating - Invalid Version Header") } } - return nil -} - -// getMessages returns an array of new messages received from the ricochet client -func (r *Ricochet) getMessages() ([]RicochetData, error) { - buf, err := r.recv() - if err != nil { - return nil, errors.New("Failed to retrieve new messages from the client") - } - - pos := 0 - finished := false - datas := []RicochetData{} - - for !finished { - size := int(binary.BigEndian.Uint16(buf[pos+0 : pos+2])) - channel := int(binary.BigEndian.Uint16(buf[pos+2 : pos+4])) - - if pos+size > len(buf) { - return datas, errors.New("Partial data packet received") - } - - data := RicochetData{ - Channel: int32(channel), - Data: buf[pos+4 : pos+size], - } - - datas = append(datas, data) - pos += size - if pos >= len(buf) { - finished = true - } - } - r.logger.Printf("Got %d Packets", len(datas)) - return datas, nil -} - -// recv reads data from the client, and returns the raw byte array, else error. -func (r *Ricochet) recv() ([]byte, error) { - buf := make([]byte, 4096) - n, err := r.conn.Read(buf) - if err != nil { - return nil, err - } - ret := make([]byte, n) - copy(ret[:], buf[:]) - return ret, nil -} - -func (r *Ricochet) handleFatal(err error, message string) { - if err != nil { - r.logger.Fatal(message) - } + + oc := new(OpenConnection) + oc.Init(outbound, conn) + return oc, nil } diff --git a/ricochetservice.go b/ricochetservice.go index 929038d..6bc075e 100644 --- a/ricochetservice.go +++ b/ricochetservice.go @@ -1,16 +1,35 @@ package goricochet +// RicochetService provides an interface for building automated ricochet applications. type RicochetService interface { - OnConnect(serverHostname string) - OnAuthenticationChallenge(channelID int32, serverHostname string, serverCookie [16]byte) - OnAuthenticationResult(channelID int32, serverHostname string, result bool) + OnReady() + OnConnect(oc *OpenConnection) - OnOpenChannelRequest(channelID int32, serverHostname string) - OnOpenChannelRequestAck(channelID int32, serverHostname string, result bool) - OnChannelClose(channelID int32, serverHostname string) + // 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) - OnContactRequest(channelID string, serverHostname string, nick string, message string) + // Contact Management + IsKnownContact(hostname string) bool + OnContactRequest(oc *OpenConnection, channelID int32, nick string, message string) + OnContactRequestAck(oc *OpenConnection, channelID int32, status string) - OnChatMessage(channelID int32, serverHostname string, messageID int32, message string) - OnChatMessageAck(channelID int32, serverHostname string, messageID int32) + // 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) } diff --git a/standardricochetservice.go b/standardricochetservice.go index fee2cbe..0ade7dc 100644 --- a/standardricochetservice.go +++ b/standardricochetservice.go @@ -1,89 +1,178 @@ package goricochet import ( - "crypto" "crypto/rsa" "crypto/x509" "encoding/asn1" - //"encoding/binary" "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 - authHandler map[string]*AuthenticationHandler - privateKey *rsa.PrivateKey - hostname string + ricochet *Ricochet + privateKey *rsa.PrivateKey + serverHostname string } -func (srs *StandardRicochetService) Init(filename string, hostname 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(true) - srs.authHandler = make(map[string]*AuthenticationHandler) - srs.hostname = hostname + srs.ricochet.Init() pemData, err := ioutil.ReadFile(filename) if err != nil { - // r.logger.Print("Error Reading Private Key: ", err) + 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" { - // r.logger.Print("No valid PEM data found") + return errors.New("Could not setup ricochet service: no valid PEM data found") } - srs.privateKey, _ = x509.ParsePKCS1PrivateKey(block.Bytes) + 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 } -func (srs *StandardRicochetService) OnConnect(serverHostname string) { - srs.authHandler[serverHostname] = new(AuthenticationHandler) - clientCookie := srs.authHandler[serverHostname].GenClientCookie() - srs.ricochet.Authenticate(1, clientCookie) +// 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) + } + oc.MyHostname = srs.serverHostname + return nil +} + +// OnConnect is called when a client or server sucessfully 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 + } +} + +// 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(channelID int32, serverHostname string, serverCookie [16]byte) { - srs.authHandler[serverHostname].AddServerCookie(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, }) - - signature, _ := rsa.SignPKCS1v15(nil, srs.privateKey, crypto.SHA256, srs.authHandler[serverHostname].GenChallenge(srs.hostname, serverHostname)) - // TODO Handle Errors - signatureBytes := make([]byte, 128) - copy(signatureBytes[:], signature[:]) - srs.ricochet.SendProof(1, publickeyBytes, signatureBytes) + oc.SendProof(1, serverCookie, publickeyBytes, srs.privateKey) } -func (srs *StandardRicochetService) Ricochet() *Ricochet { - return srs.ricochet +// 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) } -func (srs *StandardRicochetService) OnAuthenticationResult(channelID int32, serverHostname string, result bool) { - +// 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 } -func (srs *StandardRicochetService) OnOpenChannelRequest(channelID int32, serverHostname string) { - srs.ricochet.AckOpenChannel(channelID, true) +// IsKnownContact allows a caller to determine if a hostname an authorized contact. +func (srs *StandardRicochetService) IsKnownContact(hostname string) bool { + return false } -func (srs *StandardRicochetService) OnOpenChannelRequestAck(channelID int32, serverHostname string, result bool) { +// OnContactRequest is called when a client sends a new contact request +func (srs *StandardRicochetService) OnContactRequest(oc *OpenConnection, channelID int32, nick string, message string) { } -func (srs *StandardRicochetService) OnChannelClose(channelID int32, serverHostname 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) { } -func (srs *StandardRicochetService) OnContactRequest(channelID string, serverHostname string, nick string, message 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) } -func (srs *StandardRicochetService) OnChatMessage(channelID int32, serverHostname string, messageId int32, message string) { - srs.ricochet.AckChatMessage(channelID, messageId) +// OnOpenChannelRequestSuccess is called when a client or server responds to an open channel request +func (srs *StandardRicochetService) OnOpenChannelRequestSuccess(oc *OpenConnection, channelID int32) { } -func (srs *StandardRicochetService) OnChatMessageAck(channelID int32, serverHostname string, messageId int32) { +// OnChannelClose 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") } diff --git a/standardricochetservice_bad_usage_error_test.go b/standardricochetservice_bad_usage_error_test.go new file mode 100644 index 0000000..104fe7a --- /dev/null +++ b/standardricochetservice_bad_usage_error_test.go @@ -0,0 +1,108 @@ +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) + } + +} diff --git a/standardricochetservice_test.go b/standardricochetservice_test.go new file mode 100644 index 0000000..41bb91b --- /dev/null +++ b/standardricochetservice_test.go @@ -0,0 +1,107 @@ +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") + } +} diff --git a/standardricochetservice_unauth_test.go b/standardricochetservice_unauth_test.go new file mode 100644 index 0000000..df2c67c --- /dev/null +++ b/standardricochetservice_unauth_test.go @@ -0,0 +1,63 @@ +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") + } + +} diff --git a/standardricochetservice_unknown_contact_test.go b/standardricochetservice_unknown_contact_test.go new file mode 100644 index 0000000..ba49fb4 --- /dev/null +++ b/standardricochetservice_unknown_contact_test.go @@ -0,0 +1,60 @@ +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") + } + +} diff --git a/utils/error.go b/utils/error.go new file mode 100644 index 0000000..830dfbe --- /dev/null +++ b/utils/error.go @@ -0,0 +1,19 @@ +package utils + +import "fmt" +import "log" + +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) + } +} + +func CheckError(err error) { + if err != nil { + panic(fmt.Sprintf("%v", err)) + } +} diff --git a/utils/networking.go b/utils/networking.go new file mode 100644 index 0000000..9412558 --- /dev/null +++ b/utils/networking.go @@ -0,0 +1,92 @@ +package utils + +import ( + "encoding/binary" + "errors" + "net" + "strconv" +) + +// RicochetData is a structure containing the raw data and the channel it the +// message originated on. +type RicochetData struct { + Channel int32 + Data []byte +} + +// RicochetNetworkInterface abstract operations that interact with ricochet's +// packet layer. +type RicochetNetworkInterface interface { + Recv(conn net.Conn) ([]byte, error) + SendRicochetPacket(conn net.Conn, channel int32, data []byte) + RecvRicochetPackets(conn net.Conn) ([]RicochetData, error) +} + +// RicochetNetwork is a concrete implementation of the RicochetNetworkInterface +type RicochetNetwork struct { +} + +// Recv reads data from the client, and returns the raw byte array, else error. +func (rn *RicochetNetwork) Recv(conn net.Conn) ([]byte, error) { + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return nil, err + } + ret := make([]byte, n) + copy(ret[:], buf[:]) + return ret, nil +} + +// SendRicochetPacket places the data into a structure needed for the client to +// decode the packet and writes the packet to the network. +func (rn *RicochetNetwork) SendRicochetPacket(conn net.Conn, channel int32, data []byte) { + header := make([]byte, 4+len(data)) + header[0] = byte(len(header) >> 8) + header[1] = byte(len(header) & 0x00FF) + header[2] = 0x00 + header[3] = byte(channel) + copy(header[4:], data[:]) + conn.Write(header) +} + +// RecvRicochetPackets returns an array of new messages received from the ricochet client +func (rn *RicochetNetwork) RecvRicochetPackets(conn net.Conn) ([]RicochetData, error) { + buf, err := rn.Recv(conn) + if err != nil && len(buf) < 4 { + return nil, errors.New("failed to retrieve new messages from the client") + } + + pos := 0 + finished := false + var datas []RicochetData + + for !finished { + size := int(binary.BigEndian.Uint16(buf[pos+0 : pos+2])) + channel := int(binary.BigEndian.Uint16(buf[pos+2 : pos+4])) + + if size < 4 { + return datas, errors.New("invalid ricochet packet received (size=" + strconv.Itoa(size) + ")") + } + + if pos+size > len(buf) { + return datas, errors.New("partial data packet received") + } + + data := RicochetData{} + data.Channel = int32(channel) + + if pos+4 >= len(buf) { + data.Data = make([]byte, 0) + } else { + data.Data = buf[pos+4 : pos+size] + } + + datas = append(datas, data) + pos += size + if pos >= len(buf) { + finished = true + } + } + return datas, nil +} diff --git a/utils/networking_test.go b/utils/networking_test.go new file mode 100644 index 0000000..38824cf --- /dev/null +++ b/utils/networking_test.go @@ -0,0 +1,171 @@ +package utils + +import "testing" +import "net" +import "time" + +type MockConn struct { + Written []byte + MockOutput []byte +} + +func (mc *MockConn) Read(b []byte) (int, error) { + copy(b[:], mc.MockOutput[:]) + return len(mc.MockOutput), nil +} + +func (mc *MockConn) Write(written []byte) (int, error) { + mc.Written = written + return 0, nil +} + +func (mc *MockConn) LocalAddr() net.Addr { + return nil +} + +func (mc *MockConn) RemoteAddr() net.Addr { + return nil +} + +func (mc *MockConn) Close() error { + return nil +} + +func (mc *MockConn) SetDeadline(t time.Time) error { + return nil +} + +func (mc *MockConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (mc *MockConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func TestSentRicochetPacket(t *testing.T) { + conn := new(MockConn) + rni := RicochetNetwork{} + rni.SendRicochetPacket(conn, 1, []byte{}) + if len(conn.Written) != 4 && conn.Written[0] != 0x00 && conn.Written[1] != 0x00 && conn.Written[2] != 0x01 && conn.Written[3] != 0x00 { + t.Errorf("Output of SentRicochetPacket was Unexpected: %x", conn.Written) + } +} + +func TestRecv(t *testing.T) { + conn := new(MockConn) + conn.MockOutput = []byte{0xDE, 0xAD, 0xBE, 0xEF} + rni := RicochetNetwork{} + buf, err := rni.Recv(conn) + if err != nil || len(buf) != 4 || buf[0] != 0xDE || buf[1] != 0xAD || buf[2] != 0xBE || buf[3] != 0xEF { + t.Errorf("Output of Recv was Unexpected: %x", buf) + } +} + +func TestRecvRicochetPacket(t *testing.T) { + conn := new(MockConn) + conn.MockOutput = []byte{00, 0x04, 0x00, 0x01} + + rni := RicochetNetwork{} + rp, err := rni.RecvRicochetPackets(conn) + + if err != nil { + t.Errorf("error extracting ricochet packets: %v", err) + return + } + + if len(rp) != 1 { + t.Errorf("unexpected number of ricochet packets: %d", len(rp)) + } else { + if rp[0].Channel != 1 { + t.Errorf("channel number is Unexpected expected 1: %d", rp[0].Channel) + } + + if len(rp[0].Data) != 0 { + t.Errorf("expected emptry packet, instead got %x", rp[0].Data) + } + } + +} + +func TestRecvRicochetPacketInvalid(t *testing.T) { + conn := new(MockConn) + conn.MockOutput = []byte{00, 0x01, 0x00, 0x01} + + rni := RicochetNetwork{} + _, err := rni.RecvRicochetPackets(conn) + + if err == nil { + t.Errorf("recv should have errored due to invalid packets %v", err) + } + + conn.MockOutput = []byte{00, 0x0A, 0x00, 0x01} + + _, err = rni.RecvRicochetPackets(conn) + + if err == nil { + t.Errorf("recv should have errored due to invalid packets %v", err) + } + +} + +func TestRecvRicochetPacketLong(t *testing.T) { + conn := new(MockConn) + conn.MockOutput = []byte{0x00, 0x08, 0x00, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF} + + rni := RicochetNetwork{} + rp, err := rni.RecvRicochetPackets(conn) + + if err != nil { + t.Errorf("error extracting ricochet packets: %v", err) + return + } + + if len(rp) != 1 { + t.Errorf("unexpected number of ricochet packets: %d", len(rp)) + } else { + if rp[0].Channel != 255 { + t.Errorf("channel number is Unexpected expected 255 got: %d", rp[0].Channel) + } + + if len(rp[0].Data) != 4 || rp[0].Data[0] != 0xDE || rp[0].Data[1] != 0xAD || rp[0].Data[2] != 0xBE || rp[0].Data[3] != 0xEF { + t.Errorf("expected 0xDEADBEEF packet, instead got %x", rp[0].Data) + } + } + +} + +func TestRecvRicochetPacketMultiplex(t *testing.T) { + conn := new(MockConn) + conn.MockOutput = []byte{0x00, 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF} + + rni := RicochetNetwork{} + rp, err := rni.RecvRicochetPackets(conn) + + if err != nil { + t.Errorf("error extracting ricochet packets: %v", err) + return + } + + if len(rp) != 2 { + t.Errorf("unexpected number of ricochet packets, expected 2 gt: %d", len(rp)) + } else { + + if rp[0].Channel != 1 { + t.Errorf("channel number is Unexpected expected 1: %d", rp[0].Channel) + } + + if len(rp[0].Data) != 0 { + t.Errorf("expected empty packet, instead got %x", rp[0].Data) + } + + if rp[1].Channel != 255 { + t.Errorf("channel number is Unexpected expected 255 got: %d", rp[0].Channel) + } + + if len(rp[1].Data) != 4 || rp[1].Data[0] != 0xDE || rp[1].Data[1] != 0xAD || rp[1].Data[2] != 0xBE || rp[1].Data[3] != 0xEF { + t.Errorf("expected 0xDEADBEEF packet, instead got %x", rp[0].Data) + } + } + +} diff --git a/networkresolver.go b/utils/networkresolver.go similarity index 95% rename from networkresolver.go rename to utils/networkresolver.go index 3ba408c..021c568 100644 --- a/networkresolver.go +++ b/utils/networkresolver.go @@ -1,4 +1,4 @@ -package goricochet +package utils import ( "errors" @@ -43,5 +43,6 @@ func (nr *NetworkResolver) Resolve(hostname string) (net.Conn, string, error) { if err != nil { return nil, "", errors.New("Cannot Dial Remote Ricochet Address") } + //conn.SetDeadline(time.Now().Add(5 * time.Second)) return conn, resolvedHostname, nil } diff --git a/utils/tor.go b/utils/tor.go new file mode 100644 index 0000000..28ab056 --- /dev/null +++ b/utils/tor.go @@ -0,0 +1,19 @@ +package utils + +import ( + "crypto/sha1" + "encoding/base32" + "strings" +) + +// GetTorHostname takes a []byte contained a DER-encoded RSA public key +// and returns the first 16 bytes of the base32 encoded sha1 hash of the key. +// This is the onion hostname of the tor service represented by the public key. +func GetTorHostname(publicKeyBytes []byte) string { + h := sha1.New() + h.Write(publicKeyBytes) + sha1bytes := h.Sum(nil) + + data := base32.StdEncoding.EncodeToString(sha1bytes) + return strings.ToLower(data[0:16]) +} diff --git a/utils/tor_test.go b/utils/tor_test.go new file mode 100644 index 0000000..717cb44 --- /dev/null +++ b/utils/tor_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "testing" +) + +const privateKeyData = `-----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-----` + +func TestGetTorHostname(t *testing.T) { + + block, _ := pem.Decode([]byte(privateKeyData)) + privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + + // DER Encode the Public Key + publicKeyBytes, _ := asn1.Marshal(rsa.PublicKey{ + N: privateKey.PublicKey.N, + E: privateKey.PublicKey.E, + }) + + hostname := GetTorHostname(publicKeyBytes) + t.Log(hostname) + if hostname != "kwke2hntvyfqm7dr" { + t.Errorf("Hostname %s does not equal %s", hostname, "kwke2hntvyfqm7dr") + } +}