diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..110195c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: go + +script: go get github.com/golang/protobuf/proto && go get h12.me/socks && go test diff --git a/authhandler.go b/authhandler.go new file mode 100644 index 0000000..4fd67a0 --- /dev/null +++ b/authhandler.go @@ -0,0 +1,48 @@ +package goricochet + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "io" +) + +type AuthenticationHandler struct { + clientCookie [16]byte + serverCookie [16]byte +} + +func (ah *AuthenticationHandler) AddClientCookie(cookie []byte) { + copy(ah.clientCookie[:], cookie[:16]) +} + +func (ah *AuthenticationHandler) AddServerCookie(cookie []byte) { + copy(ah.serverCookie[:], cookie[:16]) +} + +func (ah *AuthenticationHandler) GenRandom() [16]byte { + var cookie [16]byte + io.ReadFull(rand.Reader, cookie[:]) + return cookie +} + +func (ah *AuthenticationHandler) GenClientCookie() [16]byte { + ah.clientCookie = ah.GenRandom() + return ah.clientCookie +} + +func (ah *AuthenticationHandler) GenServerCookie() [16]byte { + ah.serverCookie = ah.GenRandom() + return ah.serverCookie +} + +func (ah *AuthenticationHandler) GenChallenge(clientHostname string, serverHostname string) []byte { + key := make([]byte, 32) + copy(key[0:16], ah.clientCookie[:]) + copy(key[16:], ah.serverCookie[:]) + value := []byte(clientHostname + serverHostname) + mac := hmac.New(sha256.New, key) + mac.Write(value) + hmac := mac.Sum(nil) + return hmac +} diff --git a/authhandler_test.go b/authhandler_test.go new file mode 100644 index 0000000..e934a3d --- /dev/null +++ b/authhandler_test.go @@ -0,0 +1,16 @@ +package goricochet + +import "testing" +import "bytes" + +func TestAuthHandler(t *testing.T) { + authHandler := new(AuthenticationHandler) + authHandler.AddClientCookie([]byte("abcdefghijklmnop")) + authHandler.AddServerCookie([]byte("qrstuvwxyz012345")) + challenge := authHandler.GenChallenge("test.onion", "notareal.onion") + expectedChallenge := []byte{0xf5, 0xdb, 0xfd, 0xf0, 0x3d, 0x94, 0x14, 0xf1, 0x4b, 0x37, 0x93, 0xe2, 0xa5, 0x11, 0x4a, 0x98, 0x31, 0x90, 0xea, 0xb8, 0x95, 0x7a, 0x2e, 0xaa, 0xd0, 0xd2, 0x0c, 0x74, 0x95, 0xba, 0xab, 0x73} + t.Log(challenge, expectedChallenge) + if bytes.Compare(challenge[:], expectedChallenge[:]) != 0 { + t.Errorf("AuthenticationHandler Challenge Is Invalid, Got %x, Expected %x", challenge, expectedChallenge) + } +} diff --git a/controlbuilder.go b/controlbuilder.go new file mode 100644 index 0000000..74c30a8 --- /dev/null +++ b/controlbuilder.go @@ -0,0 +1,37 @@ +package goricochet + +import ( + "github.com/s-rah/go-ricochet/control" + "github.com/s-rah/go-ricochet/auth" + "github.com/golang/protobuf/proto" +) + +type ControlBuilder struct { + +} + +func (cb *ControlBuilder) OpenChatChannel(channelId int32) ([]byte,error) { + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(channelId), + ChannelType: proto.String("im.ricochet.chat"), + } + pc := &Protocol_Data_Control.Packet{ + OpenChannel: oc, + } + return proto.Marshal(pc) +} + +func (cb* ControlBuilder) OpenAuthenticationChannel(channelId int32, clientCookie [16]byte) ([]byte,error) { + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(channelId), + ChannelType: proto.String("im.ricochet.auth.hidden-service" ), + } + err := proto.SetExtension(oc, Protocol_Data_AuthHiddenService.E_ClientCookie, clientCookie[:]) + if err != nil { + return nil, err + } + pc := &Protocol_Data_Control.Packet{ + OpenChannel: oc, + } + return proto.Marshal(pc) +} diff --git a/examples/echobot/main.go b/examples/echobot/main.go index c61adac..940f481 100644 --- a/examples/echobot/main.go +++ b/examples/echobot/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "github.com/s-rah/go-ricochet" + "time" ) func main() { @@ -17,7 +18,8 @@ func main() { // ricochet.SendContactRequest("EchoBot", "I'm an EchoBot") go ricochet.ListenAndWait() - ricochet.OpenChannel("im.ricochet.chat", 5) + ricochet.OpenChatChannel(5) + time.Sleep(time.Second * 1) ricochet.SendMessage("Hi I'm an echo bot, I echo what you say! ", 5) for true { diff --git a/networkresolver.go b/networkresolver.go new file mode 100644 index 0000000..884d932 --- /dev/null +++ b/networkresolver.go @@ -0,0 +1,47 @@ +package goricochet + +import ( + "errors" + "h12.me/socks" + "net" + "strings" +) + +// NetworkResolver allows a client to resolve various hostnames to connections +// The supported types are onions address are: +// * ricochet:jlq67qzo6s4yp3sp +// * jlq67qzo6s4yp3sp +// * 127.0.0.1:55555|jlq67qzo6s4yp3sp - Localhost Connection +type NetworkResolver struct { +} + +// Resolve takes a hostname and returns a net.Conn to the derived endpoint +func (nr *NetworkResolver) Resolve(hostname string) (net.Conn, string, error) { + if strings.HasPrefix(hostname, "127.0.0.1") { + addrParts := strings.Split(hostname, "|") + tcpAddr, err := net.ResolveTCPAddr("tcp", addrParts[0]) + if err != nil { + return nil, "", errors.New("Cannot Resolve Local TCP Address") + } + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + return nil, "", errors.New("Cannot Dial Local TCP Address") + } + + // return just the onion address, not the local override for the hostname + return conn, addrParts[1], nil + } + + resolvedHostname := hostname + if strings.HasPrefix(hostname, "ricochet:") { + addrParts := strings.Split(hostname, ":") + resolvedHostname = addrParts[1] + } + + dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:9050") + conn, err := dialSocksProxy("", resolvedHostname+".onion:9878") + if err != nil { + return nil, "", errors.New("Cannot Dial Remote Ricochet Address") + } + return conn, resolvedHostname, nil +} diff --git a/ricochet.go b/ricochet.go index 8608312..e0350b7 100644 --- a/ricochet.go +++ b/ricochet.go @@ -2,10 +2,7 @@ package goricochet import ( "crypto" - "crypto/hmac" - "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/binary" @@ -17,13 +14,10 @@ import ( "github.com/s-rah/go-ricochet/chat" "github.com/s-rah/go-ricochet/contact" "github.com/s-rah/go-ricochet/control" - "h12.me/socks" - "io" "io/ioutil" "log" "net" "os" - "strings" ) // MessageType details the different kinds of messages used by Ricochet @@ -96,6 +90,19 @@ func (r *Ricochet) Init(filename string, debugLog bool) { r.channel = make(chan RicochetMessage) } +func (r *Ricochet) StartService(server RicochetService, port string) { + // Listen + ln, _ := net.Listen("tcp", port) + conn, _ := ln.Accept() + go r.runService(conn, server) +} + +func (r *Ricochet) runService(conn net.Conn, server RicochetService) { + // Negotiate Version + + // Loop For Messages +} + // Connect sets up a ricochet connection between from and to which are // both ricochet formated hostnames e.g. qn6uo4cmsrfv4kzq.onion. If this // function finished successfully then the connection can be assumed to @@ -103,51 +110,28 @@ func (r *Ricochet) Init(filename string, debugLog bool) { // To specify a local port using the format "127.0.0.1:[port]|ricochet-id". func (r *Ricochet) Connect(from string, to string) error { - if strings.HasPrefix(to, "127.0.0.1") { - toAddr := strings.Split(to, "|") - tcpAddr, err := net.ResolveTCPAddr("tcp", toAddr[0]) - if err != nil { - return errors.New("Cannot Resolve Local TCP Address") - } - r.conn, err = net.DialTCP("tcp", nil, tcpAddr) - if err != nil { - return errors.New("Cannot Dial Local TCP Address") - } - r.logger.Print("Connected to " + to + " as " + toAddr[1]) - to = toAddr[1] - } else { - dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:9050") - r.logger.Print("Connecting to ", to+".onion:9878") - conn, err := dialSocksProxy("", to+".onion:9878") - if err != nil { - return errors.New("Cannot Dial Remote Ricochet Address") - } - r.conn = conn - r.logger.Print("Connected to ", to+".onion:9878") + var err error + networkResolver := new(NetworkResolver) + r.conn, to, err = networkResolver.Resolve(to) + + if err != nil { + return err } r.negotiateVersion() - // Construct an Open Channel Message - oc := &Protocol_Data_Control.OpenChannel{ - ChannelIdentifier: proto.Int32(1), - ChannelType: proto.String("im.ricochet.auth.hidden-service"), - } - var cookie [16]byte - io.ReadFull(rand.Reader, cookie[:]) - err := proto.SetExtension(oc, Protocol_Data_AuthHiddenService.E_ClientCookie, cookie[:]) - pc := &Protocol_Data_Control.Packet{ - OpenChannel: oc, - } - data, err := proto.Marshal(pc) + authHandler := new(AuthenticationHandler) + clientCookie := authHandler.GenClientCookie() + + controlBuilder := new(ControlBuilder) + data, err := controlBuilder.OpenAuthenticationChannel(1, clientCookie) if err != nil { return errors.New("Cannot Marshal Open Channel Message") } r.sendPacket(data, 0) - r.logger.Print("Opening Channel: ", pc) response, _ := r.getMessages() openChannelResponse, _ := r.decodePacket(response[0], CONTROL) @@ -159,20 +143,7 @@ func (r *Ricochet) Connect(from string, to string) error { } sCookie, _ := proto.GetExtension(channelResult, Protocol_Data_AuthHiddenService.E_ServerCookie) - serverCookie, _ := sCookie.([]byte) - - r.logger.Print("Starting Authentication with Server Cookie: ", serverCookie) - - key := make([]byte, 32) - copy(key[0:16], cookie[:]) - copy(key[16:], serverCookie) - value := []byte(from + to) - r.logger.Print("Got Hmac Key: ", key) - r.logger.Print("Got Proof Value: ", string(value)) - mac := hmac.New(sha256.New, key) - mac.Write(value) - hmac := mac.Sum(nil) - r.logger.Print("Got HMAC: ", hmac) + authHandler.AddServerCookie(sCookie.([]byte)) // DER Encode the Public Key publickeybytes, err := asn1.Marshal(rsa.PublicKey{ @@ -180,13 +151,11 @@ func (r *Ricochet) Connect(from string, to string) error { E: r.privateKey.PublicKey.E, }) - signature, _ := rsa.SignPKCS1v15(nil, r.privateKey, crypto.SHA256, hmac) + signature, _ := rsa.SignPKCS1v15(nil, r.privateKey, crypto.SHA256, authHandler.GenChallenge(from, to)) + signatureBytes := make([]byte, 128) copy(signatureBytes[:], signature[:]) - r.logger.Print("Signature Length: ", len(signatureBytes)) - r.logger.Print("Public Key Length: ", len(publickeybytes), ", Bit Size: ", r.privateKey.PublicKey.N.BitLen()) - // Construct a Proof Message proof := &Protocol_Data_AuthHiddenService.Proof{ PublicKey: publickeybytes, @@ -218,23 +187,19 @@ func (r *Ricochet) Connect(from string, to string) error { return nil } -// OpenChannel opens a new channel with the given type and id +// 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 (currently this is the -// only supported option. -func (r *Ricochet) OpenChannel(channelType string, id int) error { - oc := &Protocol_Data_Control.OpenChannel{ - ChannelIdentifier: proto.Int32(int32(id)), - ChannelType: proto.String(channelType), +// * If acting as the client, id must be odd, else even +func (r *Ricochet) OpenChatChannel(id int32) error { + controlBuilder := new(ControlBuilder) + data,err := controlBuilder.OpenChatChannel(id) + + if err != nil { + return errors.New("error constructing control channel message to open channel") } - pc := &Protocol_Data_Control.Packet{ - OpenChannel: oc, - } - - data, _ := proto.Marshal(pc) - r.logger.Printf("Opening %s Channel: %d", channelType, id) + r.logger.Printf("Opening Chat Channel: %d", id) r.sendPacket(data, 0) return nil } @@ -319,6 +284,7 @@ func (r *Ricochet) sendPacket(data []byte, channel int) { header[2] = 0x00 header[3] = byte(channel) copy(header[4:], data[:]) + fmt.Fprintf(r.conn, "%s", header) } diff --git a/ricochetservice.go b/ricochetservice.go new file mode 100644 index 0000000..8186553 --- /dev/null +++ b/ricochetservice.go @@ -0,0 +1,7 @@ +package goricochet + +type RicochetService interface { + OnConnect(id string) error + OnContactRequest(id string) error + OnMessage(id string, message string, channel int) error +}