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