diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..01febfc --- /dev/null +++ b/.drone.yml @@ -0,0 +1,44 @@ +workspace: + base: /go + path: src/cwtch.im/tapir + +pipeline: + fetch: + image: golang + commands: + - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor + - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc + - chmod a+x tor + - go list ./... | xargs go get + - go get -u golang.org/x/lint/golint + quality: + image: golang + commands: + - go list ./... | xargs go vet + - go list ./... | xargs golint -set_exit_status + units-tests: + image: golang + commands: + - export PATH=$PATH:/go/src/cwtch.im/tapir + - sh testing/tests.sh + integ-test: + image: golang + commands: + - ./tor -f ./torrc + - sleep 15 + - go test -v cwtch.im/tapir/testing + notify-email: + image: drillster/drone-email + host: build.openprivacy.ca + port: 25 + skip_verify: true + from: drone@openprivacy.ca + when: + status: [ failure ] + notify-gogs: + image: openpriv/drone-gogs + when: + event: pull_request + status: [ success, changed, failure ] + secrets: [gogs_account_token] + gogs_url: https://git.openprivacy.ca diff --git a/.gitignore b/.gitignore index 5089fe7..e621b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ .idea /tor/ +coverage.out diff --git a/application.go b/application.go index 5efc9be..505ac1f 100644 --- a/application.go +++ b/application.go @@ -3,5 +3,5 @@ package tapir // Application defines the interface for all Tapir Applications type Application interface { NewInstance() Application - Init(connection *Connection) + Init(connection Connection) } diff --git a/applications/auth.go b/applications/auth.go index 4306394..2004f5f 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -33,8 +33,8 @@ func (ea AuthApp) NewInstance() tapir.Application { // Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability // or the connection is closed. -func (ea AuthApp) Init(connection *tapir.Connection) { - longTermPubKey := ed25519.PublicKey(connection.ID.PublicKeyBytes()) +func (ea AuthApp) Init(connection tapir.Connection) { + longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes()) epk, esk, _ := ed25519.GenerateKey(rand.Reader) ephemeralPublicKey := ed25519.PublicKey(epk) ephemeralPrivateKey := ed25519.PrivateKey(esk) @@ -52,13 +52,13 @@ func (ea AuthApp) Init(connection *tapir.Connection) { } // 3DH Handshake - l2e := connection.ID.EDH(remoteAuthMessage.EphemeralPublicKey) + l2e := connection.ID().EDH(remoteAuthMessage.EphemeralPublicKey) e2l := ephemeralIdentity.EDH(remoteAuthMessage.LongTermPublicKey) e2e := ephemeralIdentity.EDH(remoteAuthMessage.EphemeralPublicKey) // We need to define an order for the result concatenation so that both sides derive the same key. var result [96]byte - if connection.Outbound { + if connection.IsOutbound() { copy(result[0:32], l2e) copy(result[32:64], e2l) copy(result[64:96], e2e) @@ -69,15 +69,24 @@ func (ea AuthApp) Init(connection *tapir.Connection) { } connection.SetEncryptionKey(sha3.Sum256(result[:])) - // Wait to Sync + // Wait to Sync (we need to ensure that both the Local and Remote server have turned encryption on + // otherwise our next Send will fail. time.Sleep(time.Second) - // TODO: Replace this with proper transcript + // TODO: Replace this with proper transcript primitive challengeRemote, err := json.Marshal(remoteAuthMessage) + if err != nil { + connection.Close() + return + } challengeLocal, err := json.Marshal(authMessage) + if err != nil { + connection.Close() + return + } challenge := sha3.New512() - if connection.Outbound { + if connection.IsOutbound() { challenge.Write(challengeLocal) challenge.Write(challengeRemote) } else { diff --git a/applications/auth_test.go b/applications/auth_test.go new file mode 100644 index 0000000..7a07781 --- /dev/null +++ b/applications/auth_test.go @@ -0,0 +1,83 @@ +package applications + +import ( + "crypto/rand" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/identity" + "golang.org/x/crypto/ed25519" + "testing" +) + +type MockConnection struct { + id identity.Identity + outbound bool +} + +func (mc *MockConnection) Init(outbound bool) { + pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader) + sk := ed25519.PrivateKey(privateKey) + pk := ed25519.PublicKey(pubkey) + mc.id = identity.InitializeV3("", &sk, &pk) + mc.outbound = outbound + return +} + +func (MockConnection) Hostname() string { + panic("implement me") +} + +func (mc MockConnection) IsOutbound() bool { + return mc.outbound +} + +func (mc MockConnection) ID() *identity.Identity { + return &mc.id +} + +func (mc MockConnection) Expect() []byte { + longTermPubKey := ed25519.PublicKey(mc.id.PublicKeyBytes()) + epk, _, _ := ed25519.GenerateKey(rand.Reader) + ephemeralPublicKey := ed25519.PublicKey(epk) + //ephemeralPrivateKey := ed25519.PrivateKey(esk) + //ephemeralIdentity := identity.InitializeV3("", &ephemeralPrivateKey, &ephemeralPublicKey) + authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralPublicKey} + serialized, _ := json.Marshal(authMessage) + return serialized +} + +func (MockConnection) SetHostname(hostname string) { + panic("implement me") +} + +func (MockConnection) HasCapability(name string) bool { + panic("implement me") +} + +func (MockConnection) SetCapability(name string) { + panic("implement me") +} + +func (MockConnection) SetEncryptionKey(key [32]byte) { + // no op +} + +func (MockConnection) Send(message []byte) { + // no op +} + +func (MockConnection) Close() { + // no op +} + +func (MockConnection) IsClosed() bool { + panic("implement me") +} + +func TestAuthApp_Failed(t *testing.T) { + var authApp AuthApp + ai := authApp.NewInstance() + + mc := new(MockConnection) + mc.Init(true) + ai.Init(mc) +} diff --git a/cmd/main.go b/cmd/main.go index 0cc56bb..4becb78 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,7 +25,7 @@ func (ea SimpleApp) NewInstance() tapir.Application { } // Init is run when the connection is first started. -func (ea SimpleApp) Init(connection *tapir.Connection) { +func (ea SimpleApp) Init(connection tapir.Connection) { // First run the Authentication App ea.AuthApp.Init(connection) @@ -44,9 +44,8 @@ func CheckConnection(service tapir.Service, hostname string) { if err == nil { log.Infof("Authed!") return - } else { - log.Errorf("Error %v", err) } + log.Errorf("Error %v", err) time.Sleep(time.Second) } } diff --git a/networks/tor/BaseOnionService.go b/networks/tor/BaseOnionService.go index 52ce6c0..a13a66e 100644 --- a/networks/tor/BaseOnionService.go +++ b/networks/tor/BaseOnionService.go @@ -34,14 +34,14 @@ func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id // WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed // (through error or user action) -func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (*tapir.Connection, error) { +func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (tapir.Connection, error) { conn, err := s.GetConnection(cid) if err == nil { for { if conn.HasCapability(name) { return conn, nil } - if conn.Closed { + if conn.IsClosed() { return nil, errors.New("connection is closed") } time.Sleep(time.Millisecond * 200) @@ -51,12 +51,12 @@ func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (*t } // GetConnection returns a connection for a given hostname. -func (s *BaseOnionService) GetConnection(hostname string) (*tapir.Connection, error) { - var conn *tapir.Connection +func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) { + var conn tapir.Connection s.connections.Range(func(key, value interface{}) bool { - connection := value.(*tapir.Connection) - if connection.Hostname == hostname { - if !connection.Closed { + connection := value.(tapir.Connection) + if connection.Hostname() == hostname { + if !connection.IsClosed() { conn = connection return false } @@ -136,10 +136,11 @@ func (s *BaseOnionService) Listen(app tapir.Application) error { return err } +// Shutdown closes the service and ensures that any connections are closed. func (s *BaseOnionService) Shutdown() { s.ls.Close() s.connections.Range(func(key, value interface{}) bool { - connection := value.(*tapir.Connection) + connection := value.(tapir.Connection) connection.Close() return true }) diff --git a/notifications/main.go b/notifications/main.go deleted file mode 100644 index 71ad6ab..0000000 --- a/notifications/main.go +++ /dev/null @@ -1,192 +0,0 @@ -package main - -import ( - "bytes" - "compress/gzip" - "crypto/rand" - "crypto/sha512" - "cwtch.im/tapir" - "cwtch.im/tapir/applications" - "cwtch.im/tapir/networks/tor" - "cwtch.im/tapir/primitives" - "encoding/hex" - "encoding/json" - "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "git.openprivacy.ca/openprivacy/libricochet-go/identity" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "git.openprivacy.ca/openprivacy/libricochet-go/utils" - "golang.org/x/crypto/ed25519" - "io/ioutil" - "os" - "time" -) - -// This example implements a basic notification application which allows peers to notify each other of new messages without downloading -// the entire contents of the server. -// NOTE: Very Incomplete Prototype. - -// Notification contains a Topic string and a Message. -type Notification struct { - Topic string // A hex encoded string of the hash of the topic string - Message string -} - -// NotificationClient allows publishing and reading from the notifications server -type NotificationClient struct { - applications.AuthApp - connection *tapir.Connection -} - -// NewInstance should always return a new instantiation of the application. -func (nc NotificationClient) NewInstance() tapir.Application { - app := new(NotificationClient) - return app -} - -// Init is run when the connection is first started. -func (nc *NotificationClient) Init(connection *tapir.Connection) { - // First run the Authentication App - nc.AuthApp.Init(connection) - if connection.HasCapability(applications.AuthCapability) { - nc.connection = connection - } -} - -// Publish transforms the given topic string into a hashed ID, and sends the ID along with the message -// NOTE: Server learns the hash of the topic (and therefore can correlate repeated use of the same topic) -func (nc NotificationClient) Publish(topic string, message string) { - log.Debugf("Sending Publish Request") - hashedTopic := sha512.Sum512([]byte(topic)) - data, _ := json.Marshal(notificationRequest{RequestType: "Publish", RequestData: map[string]string{"Topic": hex.EncodeToString(hashedTopic[:])}}) - nc.connection.Send([]byte(data)) -} - -// Check returns true if the server might have notifications related to the topic. -// This check reveals nothing about the topic to the server. -func (nc NotificationClient) Check(topic string) bool { - log.Debugf("Sending Filter Request") - // Get an updated bloom filter - data, _ := json.Marshal(notificationRequest{RequestType: "BloomFilter", RequestData: map[string]string{}}) - nc.connection.Send(data) - response := nc.connection.Expect() - var bf []primitives.BloomFilter - r, _ := gzip.NewReader(bytes.NewReader(response)) - bfb, _ := ioutil.ReadAll(r) - json.Unmarshal(bfb, &bf) - - // Check the topic handle in the bloom filter - hashedTopic := sha512.Sum512([]byte(topic)) - return bf[time.Now().Hour()].Check(hashedTopic[:]) -} - -type notificationRequest struct { - RequestType string - RequestData map[string]string -} - -// NotificationsServer implements the metadata resistant notifications server -type NotificationsServer struct { - applications.AuthApp - Filter []*primitives.BloomFilter - timeProvider primitives.TimeProvider -} - -const DefaultNumberOfBuckets = 24 // 1 per hour of the day - -// NewInstance should always return a new instantiation of the application. -func (ns NotificationsServer) NewInstance() tapir.Application { - app := new(NotificationsServer) - - app.timeProvider = new(primitives.OSTimeProvider) - app.Filter = make([]*primitives.BloomFilter, DefaultNumberOfBuckets) - for i := range app.Filter { - app.Filter[i] = new(primitives.BloomFilter) - app.Filter[i].Init(1024) - } - return app -} - -// Configure overrides the default parameters for the Notification Server -func (ns NotificationsServer) Configure(timeProvider primitives.TimeProvider) { - ns.timeProvider = timeProvider -} - -// Init initializes the application. -func (ns NotificationsServer) Init(connection *tapir.Connection) { - // First run the Authentication App - ns.AuthApp.Init(connection) - if connection.HasCapability(applications.AuthCapability) { - for { - request := connection.Expect() - var nr notificationRequest - json.Unmarshal(request, &nr) - log.Debugf("Received Request %v", nr) - switch nr.RequestType { - case "Publish": - log.Debugf("Received Publish Request") - topic := nr.RequestData["Topic"] - // message := nr.RequestData["Message"] - topicID, err := hex.DecodeString(topic) - if err == nil { - currentBucket := ns.timeProvider.GetCurrentTime().Hour() - ns.Filter[currentBucket].Insert(topicID) - } - case "BloomFilter": - log.Debugf("Received Filter Request") - response, _ := json.Marshal(ns.Filter) - var b bytes.Buffer - w := gzip.NewWriter(&b) - w.Write(response) - w.Close() - connection.Send(b.Bytes()) - } - } - } -} - -func main() { - - log.SetLevel(log.LevelDebug) - - // Connect to Tor - var acn connectivity.ACN - acn, _ = connectivity.StartTor("./", "") - acn.WaitTillBootstrapped() - - // Generate Server Keys - pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader) - sk := ed25519.PrivateKey(privateKey) - pk := ed25519.PublicKey(pubkey) - id := identity.InitializeV3("server", &sk, &pk) - - // Init a Client to Connect to the Server - go client(acn, pubkey) - - rm -} - -// Client will Connect and launch it's own Echo App goroutine. -func client(acn connectivity.ACN, key ed25519.PublicKey) { - pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader) - sk := ed25519.PrivateKey(privateKey) - pk := ed25519.PublicKey(pubkey) - id := identity.InitializeV3("client", &sk, &pk) - var client tapir.Service - client = new(tor.BaseOnionService) - client.Init(acn, sk, id) - - cid, _ := client.Connect(utils.GetTorV3Hostname(key), new(NotificationClient)) - - conn, err := client.WaitForCapabilityOrClose(cid, applications.AuthCapability) - if err == nil { - log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability)) - nc := conn.App.(*NotificationClient) - - // Basic Demonstration of Notification - log.Infof("Publishing to #astronomy: %v", nc.Check("#astronomy")) - nc.Publish("#astronomy", "New #Astronomy Post!") - log.Infof("Checking #astronomy: %v", nc.Check("#astronomy")) - } - - os.Exit(0) -} diff --git a/primitives/bloom.go b/primitives/bloom.go index fe558a9..3104a81 100644 --- a/primitives/bloom.go +++ b/primitives/bloom.go @@ -18,7 +18,7 @@ func (bf *BloomFilter) Init(m int16) { // Hash transforms a message to a set of bit flips // Supports up to m == 65535 -func (bf BloomFilter) Hash(msg []byte) []int { +func (bf *BloomFilter) Hash(msg []byte) []int { hash := sha256.Sum256(msg) pos1a := (int(hash[0]) + int(hash[1]) + int(hash[2]) + int(hash[3])) % 0xFF @@ -53,7 +53,7 @@ func (bf *BloomFilter) Insert(msg []byte) { // Check returns true if the messages might be in the BloomFilter // (No false positives, possible false negatives due to the probabilistic nature of the filter) -func (bf BloomFilter) Check(msg []byte) bool { +func (bf *BloomFilter) Check(msg []byte) bool { pos := bf.Hash(msg) if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] { return true diff --git a/primitives/time.go b/primitives/time.go index 1956360..b957a46 100644 --- a/primitives/time.go +++ b/primitives/time.go @@ -15,6 +15,7 @@ type TimeProvider interface { type OSTimeProvider struct { } +// GetCurrentTime returns the time provided by the OS func (ostp OSTimeProvider) GetCurrentTime() time.Time { return time.Now() } diff --git a/service.go b/service.go index 7a8cfa6..b8d9af2 100644 --- a/service.go +++ b/service.go @@ -18,70 +18,107 @@ type Service interface { Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity identity.Identity) Connect(hostname string, application Application) (bool, error) Listen(application Application) error - GetConnection(connectionID string) (*Connection, error) - WaitForCapabilityOrClose(connectionID string, capability string) (*Connection, error) + GetConnection(connectionID string) (Connection, error) + WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error) Shutdown() } +// Connection Interface +type Connection interface { + Hostname() string + IsOutbound() bool + ID() *identity.Identity + Expect() []byte + SetHostname(hostname string) + HasCapability(name string) bool + SetCapability(name string) + SetEncryptionKey(key [32]byte) + Send(message []byte) + Close() + IsClosed() bool +} + // Connection defines a Tapir Connection -type Connection struct { - Hostname string +type connection struct { + hostname string conn net.Conn capabilities sync.Map encrypted bool key [32]byte App Application - ID identity.Identity - Outbound bool - Closed bool + identity *identity.Identity + outbound bool + closed bool MaxLength int } // NewConnection creates a new Connection -func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) *Connection { - connection := new(Connection) - connection.Hostname = hostname +func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection { + connection := new(connection) + connection.hostname = hostname connection.conn = conn connection.App = app - connection.ID = id - connection.Outbound = outbound + connection.identity = &id + connection.outbound = outbound connection.MaxLength = 1024 go connection.App.Init(connection) return connection } +// ID returns an identity.Identity encapsulation (for the purposes of cryptographic protocols) +func (c *connection) ID() *identity.Identity { + return c.identity +} + +// Hostname returns the hostname of the connection (if the connection has not been authorized it will return the +// temporary hostname identifier) +func (c *connection) Hostname() string { + return c.hostname +} + +// IsOutbound returns true if this caller was the originator of the connection (i.e. the connection was started +// by calling Connect() rather than Accept() +func (c *connection) IsOutbound() bool { + return c.outbound +} + +// IsClosed returns true if the connection is closed (connections cannot be reopened) +func (c *connection) IsClosed() bool { + return c.closed +} + // SetHostname sets the hostname on the connection -func (c *Connection) SetHostname(hostname string) { - log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.ID.Hostname(), c.Hostname, hostname) - c.Hostname = hostname +func (c *connection) SetHostname(hostname string) { + log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.identity.Hostname(), c.hostname, hostname) + c.hostname = hostname } // SetCapability sets a capability on the connection -func (c *Connection) SetCapability(name string) { - log.Debugf("[%v -- %v] Setting Capability %v", c.ID.Hostname(), c.Hostname, name) +func (c *connection) SetCapability(name string) { + log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name) c.capabilities.Store(name, true) } // HasCapability checks if the connection has a given capability -func (c *Connection) HasCapability(name string) bool { +func (c *connection) HasCapability(name string) bool { _, ok := c.capabilities.Load(name) return ok } // Close forcibly closes the connection -func (c *Connection) Close() { +func (c *connection) Close() { c.conn.Close() } // Expect blocks and reads a single Tapir packet , from the connection. -func (c *Connection) Expect() []byte { +func (c *connection) Expect() []byte { buffer := make([]byte, c.MaxLength) n, err := io.ReadFull(c.conn, buffer) if n != c.MaxLength || err != nil { - log.Errorf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.Hostname, c.ID.Hostname(), n, err) + log.Errorf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.hostname, c.identity.Hostname(), n, err) c.conn.Close() - c.Closed = true + c.closed = true return []byte{} } @@ -92,9 +129,9 @@ func (c *Connection) Expect() []byte { if ok { copy(buffer, decrypted) } else { - log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.Hostname, c.ID.Hostname()) + log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.hostname, c.identity.Hostname()) c.conn.Close() - c.Closed = true + c.closed = true return []byte{} } } @@ -104,13 +141,13 @@ func (c *Connection) Expect() []byte { } // SetEncryptionKey turns on application-level encryption on the connection using the given key. -func (c *Connection) SetEncryptionKey(key [32]byte) { +func (c *connection) SetEncryptionKey(key [32]byte) { c.key = key c.encrypted = true } // Send writes a given message to a Tapir packet (of 1024 bytes in length). -func (c *Connection) Send(message []byte) { +func (c *connection) Send(message []byte) { buffer := make([]byte, c.MaxLength) binary.PutUvarint(buffer[0:2], uint64(len(message))) @@ -121,16 +158,16 @@ func (c *Connection) Send(message []byte) { if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { // TODO: Surface is Error c.conn.Close() - c.Closed = true + c.closed = true } // MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag. encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key) copy(buffer, encrypted[0:c.MaxLength]) } - log.Debugf("[%v -> %v] Wire Send %x", c.ID.Hostname(), c.Hostname, buffer) + log.Debugf("[%v -> %v] Wire Send %x", c.identity.Hostname(), c.hostname, buffer) _, err := c.conn.Write(buffer) if err != nil { c.conn.Close() - c.Closed = true + c.closed = true } } diff --git a/testing/quality.sh b/testing/quality.sh new file mode 100755 index 0000000..c913c92 --- /dev/null +++ b/testing/quality.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +echo "Checking code quality (you want to see no output here)" +echo "" + +echo "Vetting:" +go list ./... | xargs go vet + +echo "" +echo "Linting:" + +go list ./... | xargs golint + + +echo "Time to format" +gofmt -l -s -w . + +# ineffassign (https://github.com/gordonklaus/ineffassign) +echo "Checking for ineffectual assignment of errors (unchecked errors...)" +ineffassign . + +# misspell (https://github.com/client9/misspell/cmd/misspell) +echo "Checking for misspelled words..." +misspell . | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea" diff --git a/testing/tests.sh b/testing/tests.sh new file mode 100755 index 0000000..b3e6652 --- /dev/null +++ b/testing/tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e +pwd +go test ${1} -coverprofile=applications.cover.out -v ./applications +echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ +awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out +rm -rf *.cover.out