forked from cwtch.im/tapir
Compare commits
14 Commits
Author | SHA1 | Date |
---|---|---|
Dan Ballard | f38efc6bf2 | |
Dan Ballard | 3a6cd3bd79 | |
Sarah Jamie Lewis | 5af71324b2 | |
Sarah Jamie Lewis | 9c91a4e000 | |
Dan Ballard | 5e4c386847 | |
Sarah Jamie Lewis | f2958b21ca | |
Dan Ballard | 6515a4e160 | |
Sarah Jamie Lewis | 875690aff2 | |
Sarah Jamie Lewis | 164e91fa17 | |
Sarah Jamie Lewis | 42d3cb196a | |
Sarah Jamie Lewis | f664afb021 | |
Sarah Jamie Lewis | f571fad47a | |
Sarah Jamie Lewis | 63d6c1a1aa | |
Sarah Jamie Lewis | 5d6514a3de |
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
||||||
|
- export GO111MODULE=on
|
||||||
|
- go mod vendor
|
||||||
|
- 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
|
|
@ -1,3 +1,5 @@
|
||||||
vendor/
|
vendor/
|
||||||
.idea
|
.idea
|
||||||
/tor/
|
/tor/
|
||||||
|
coverage.out
|
||||||
|
/testing/tor/
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
# TAPir: Tiny Anonymous Peer
|
# TAPir: Tiny Anonymous Peer
|
||||||
|
|
||||||
|
![](tapir.png)
|
||||||
|
|
||||||
Tapir is a small library for building p2p applications over anonymous communication systems (right now Tapir only supports Tor v3 Onion Services).
|
Tapir is a small library for building p2p applications over anonymous communication systems (right now Tapir only supports Tor v3 Onion Services).
|
||||||
|
|
||||||
Tapir has been designed as a replacement to the Ricochet protocol.
|
Tapir has been designed as a replacement to the Ricochet protocol.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* New Authentication Protocol based on v3 Onion Services
|
||||||
|
* Bidirectional Application Channels.
|
||||||
|
|
||||||
**Work In Progress**
|
**Work In Progress**
|
|
@ -3,5 +3,5 @@ package tapir
|
||||||
// Application defines the interface for all Tapir Applications
|
// Application defines the interface for all Tapir Applications
|
||||||
type Application interface {
|
type Application interface {
|
||||||
NewInstance() Application
|
NewInstance() Application
|
||||||
Init(connection *Connection)
|
Init(connection Connection)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package applications
|
package applications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"cwtch.im/tapir"
|
"cwtch.im/tapir"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
|
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
|
@ -33,13 +32,10 @@ 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
|
// Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability
|
||||||
// or the connection is closed.
|
// or the connection is closed.
|
||||||
func (ea AuthApp) Init(connection *tapir.Connection) {
|
func (ea AuthApp) Init(connection tapir.Connection) {
|
||||||
longTermPubKey := ed25519.PublicKey(connection.ID.PublicKeyBytes())
|
longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes())
|
||||||
epk, esk, _ := ed25519.GenerateKey(rand.Reader)
|
ephemeralIdentity, _ := primitives.InitializeEphemeralIdentity()
|
||||||
ephemeralPublicKey := ed25519.PublicKey(epk)
|
authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()}
|
||||||
ephemeralPrivateKey := ed25519.PrivateKey(esk)
|
|
||||||
ephemeralIdentity := identity.InitializeV3("", &ephemeralPrivateKey, &ephemeralPublicKey)
|
|
||||||
authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralPublicKey}
|
|
||||||
serialized, _ := json.Marshal(authMessage)
|
serialized, _ := json.Marshal(authMessage)
|
||||||
connection.Send(serialized)
|
connection.Send(serialized)
|
||||||
message := connection.Expect()
|
message := connection.Expect()
|
||||||
|
@ -51,33 +47,36 @@ func (ea AuthApp) Init(connection *tapir.Connection) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3DH Handshake
|
// If we are an outbound connection we can perform an additional check to ensure that the server sent us back the correct long term
|
||||||
l2e := connection.ID.EDH(remoteAuthMessage.EphemeralPublicKey)
|
// public key
|
||||||
e2l := ephemeralIdentity.EDH(remoteAuthMessage.LongTermPublicKey)
|
if connection.IsOutbound() && utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) != connection.Hostname() {
|
||||||
e2e := ephemeralIdentity.EDH(remoteAuthMessage.EphemeralPublicKey)
|
log.Errorf("The remote server (%v) has attempted to authenticate with a different public key %v", connection.Hostname(), utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
|
||||||
|
connection.Close()
|
||||||
// We need to define an order for the result concatenation so that both sides derive the same key.
|
return
|
||||||
var result [96]byte
|
|
||||||
if connection.Outbound {
|
|
||||||
copy(result[0:32], l2e)
|
|
||||||
copy(result[32:64], e2l)
|
|
||||||
copy(result[64:96], e2e)
|
|
||||||
} else {
|
|
||||||
copy(result[0:32], e2l)
|
|
||||||
copy(result[32:64], l2e)
|
|
||||||
copy(result[64:96], e2e)
|
|
||||||
}
|
}
|
||||||
connection.SetEncryptionKey(sha3.Sum256(result[:]))
|
|
||||||
|
|
||||||
// Wait to Sync
|
// Perform the triple-diffie-hellman exchange.
|
||||||
|
key := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound())
|
||||||
|
connection.SetEncryptionKey(key)
|
||||||
|
|
||||||
|
// 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)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
// TODO: Replace this with proper transcript
|
// TODO: Replace this with proper transcript primitive
|
||||||
challengeRemote, err := json.Marshal(remoteAuthMessage)
|
challengeRemote, err := json.Marshal(remoteAuthMessage)
|
||||||
|
if err != nil {
|
||||||
|
connection.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
challengeLocal, err := json.Marshal(authMessage)
|
challengeLocal, err := json.Marshal(authMessage)
|
||||||
|
if err != nil {
|
||||||
|
connection.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
challenge := sha3.New512()
|
challenge := sha3.New512()
|
||||||
|
|
||||||
if connection.Outbound {
|
if connection.IsOutbound() {
|
||||||
challenge.Write(challengeLocal)
|
challenge.Write(challengeLocal)
|
||||||
challenge.Write(challengeRemote)
|
challenge.Write(challengeRemote)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package applications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"cwtch.im/tapir"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
|
"encoding/json"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockConnection struct {
|
||||||
|
id primitives.Identity
|
||||||
|
outbound bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MockConnection) Init(outbound bool) {
|
||||||
|
mc.id, _ = primitives.InitializeEphemeralIdentity()
|
||||||
|
mc.outbound = outbound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc MockConnection) Hostname() string {
|
||||||
|
return mc.id.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc MockConnection) IsOutbound() bool {
|
||||||
|
return mc.outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc MockConnection) ID() *primitives.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) App() tapir.Application {
|
||||||
|
// no op
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -3,10 +3,10 @@ package tor
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"cwtch.im/tapir"
|
"cwtch.im/tapir"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
"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/log"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -17,14 +17,14 @@ import (
|
||||||
type BaseOnionService struct {
|
type BaseOnionService struct {
|
||||||
connections sync.Map
|
connections sync.Map
|
||||||
acn connectivity.ACN
|
acn connectivity.ACN
|
||||||
id identity.Identity
|
id *primitives.Identity
|
||||||
privateKey ed25519.PrivateKey
|
privateKey ed25519.PrivateKey
|
||||||
ls connectivity.ListenService
|
ls connectivity.ListenService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes a BaseOnionService with a given private key and identity
|
// Init initializes a BaseOnionService with a given private key and identity
|
||||||
// The private key is needed to initialize the Onion listen socket, ideally we could just pass an Identity in here.
|
// The private key is needed to initialize the Onion listen socket, ideally we could just pass an Identity in here.
|
||||||
func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id identity.Identity) {
|
func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id *primitives.Identity) {
|
||||||
// run add onion
|
// run add onion
|
||||||
// get listen context
|
// get listen context
|
||||||
s.acn = acn
|
s.acn = acn
|
||||||
|
@ -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
|
// WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed
|
||||||
// (through error or user action)
|
// (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)
|
conn, err := s.GetConnection(cid)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for {
|
for {
|
||||||
if conn.HasCapability(name) {
|
if conn.HasCapability(name) {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
if conn.Closed {
|
if conn.IsClosed() {
|
||||||
return nil, errors.New("connection is closed")
|
return nil, errors.New("connection is closed")
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 200)
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
@ -51,12 +51,13 @@ func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (*t
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnection returns a connection for a given hostname.
|
// GetConnection returns a connection for a given hostname.
|
||||||
func (s *BaseOnionService) GetConnection(hostname string) (*tapir.Connection, error) {
|
func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) {
|
||||||
var conn *tapir.Connection
|
var conn tapir.Connection
|
||||||
s.connections.Range(func(key, value interface{}) bool {
|
s.connections.Range(func(key, value interface{}) bool {
|
||||||
connection := value.(*tapir.Connection)
|
log.Debugf("Checking %v", key)
|
||||||
if connection.Hostname == hostname {
|
connection := value.(tapir.Connection)
|
||||||
if !connection.Closed {
|
if connection.Hostname() == hostname {
|
||||||
|
if !connection.IsClosed() {
|
||||||
conn = connection
|
conn = connection
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -70,7 +71,7 @@ func (s *BaseOnionService) GetConnection(hostname string) (*tapir.Connection, er
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect initializes a new outbound connection to the given peer, using the defined Application
|
// Connect initializes a new outbound connection to the given peer, using the defined Application
|
||||||
func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (string, error) {
|
func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool, error) {
|
||||||
_, err := s.GetConnection(hostname)
|
_, err := s.GetConnection(hostname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Note: This check is not 100% reliable. And we may end up with two connections between peers
|
// Note: This check is not 100% reliable. And we may end up with two connections between peers
|
||||||
|
@ -78,7 +79,7 @@ func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (stri
|
||||||
// Because at the start of the connection the server cannot derive the true hostname of the client until it
|
// Because at the start of the connection the server cannot derive the true hostname of the client until it
|
||||||
// has auth'd
|
// has auth'd
|
||||||
// We mitigate this by performing multiple checks when Connect'ing
|
// We mitigate this by performing multiple checks when Connect'ing
|
||||||
return "", errors.New("already connected to " + hostname)
|
return true, errors.New("already connected to " + hostname)
|
||||||
}
|
}
|
||||||
// connects to a remote server
|
// connects to a remote server
|
||||||
// spins off to a connection struct
|
// spins off to a connection struct
|
||||||
|
@ -93,15 +94,15 @@ func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (stri
|
||||||
_, err := s.GetConnection(hostname)
|
_, err := s.GetConnection(hostname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return "", errors.New("already connected to " + hostname)
|
return true, errors.New("already connected to " + hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Connected to %v [%v]", hostname, connectionID)
|
log.Debugf("Connected to %v [%v]", hostname, connectionID)
|
||||||
s.connections.Store(connectionID, tapir.NewConnection(s.id, hostname, true, conn, app.NewInstance()))
|
s.connections.Store(connectionID, tapir.NewConnection(s.id, hostname, true, conn, app.NewInstance()))
|
||||||
return connectionID, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
log.Debugf("Error connecting to %v %v", hostname, err)
|
log.Debugf("Error connecting to %v %v", hostname, err)
|
||||||
return "", err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaseOnionService) getNewConnectionID() string {
|
func (s *BaseOnionService) getNewConnectionID() string {
|
||||||
|
@ -136,10 +137,11 @@ func (s *BaseOnionService) Listen(app tapir.Application) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown closes the service and ensures that any connections are closed.
|
||||||
func (s *BaseOnionService) Shutdown() {
|
func (s *BaseOnionService) Shutdown() {
|
||||||
s.ls.Close()
|
s.ls.Close()
|
||||||
s.connections.Range(func(key, value interface{}) bool {
|
s.connections.Range(func(key, value interface{}) bool {
|
||||||
connection := value.(*tapir.Connection)
|
connection := value.(tapir.Connection)
|
||||||
connection.Close()
|
connection.Close()
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package primitives
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Perform3DH encapsulates a triple-diffie-hellman key exchange.
|
||||||
|
// In this exchange Alice and Bob both hold longterm identity keypairs
|
||||||
|
// Both Alice and Bob generate an additional ephemeral key pair:
|
||||||
|
// Three Diffie Hellman exchanges are then performed:
|
||||||
|
// Alice Long Term <-> Bob Ephemeral
|
||||||
|
// Alice Ephemeral <-> Bob Long Term
|
||||||
|
// Alice Ephemeral <-> Bob Ephemeral
|
||||||
|
//
|
||||||
|
// Through this, a unique session key is derived. The exchange is offline-deniable (in the context of Tapir and Onion Service)
|
||||||
|
func Perform3DH(longtermIdentity *Identity, ephemeralIdentity *Identity, remoteLongTermPublicKey ed25519.PublicKey, remoteEphemeralPublicKey ed25519.PublicKey, outbound bool) [32]byte {
|
||||||
|
// 3DH Handshake
|
||||||
|
l2e := longtermIdentity.EDH(remoteEphemeralPublicKey)
|
||||||
|
e2l := ephemeralIdentity.EDH(remoteLongTermPublicKey)
|
||||||
|
e2e := ephemeralIdentity.EDH(remoteEphemeralPublicKey)
|
||||||
|
|
||||||
|
// We need to define an order for the result concatenation so that both sides derive the same key.
|
||||||
|
var result [96]byte
|
||||||
|
if outbound {
|
||||||
|
copy(result[0:32], l2e)
|
||||||
|
copy(result[32:64], e2l)
|
||||||
|
copy(result[64:96], e2e)
|
||||||
|
} else {
|
||||||
|
copy(result[0:32], e2l)
|
||||||
|
copy(result[32:64], l2e)
|
||||||
|
copy(result[64:96], e2e)
|
||||||
|
}
|
||||||
|
return sha3.Sum256(result[:])
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ func (bf *BloomFilter) Init(m int16) {
|
||||||
|
|
||||||
// Hash transforms a message to a set of bit flips
|
// Hash transforms a message to a set of bit flips
|
||||||
// Supports up to m == 65535
|
// Supports up to m == 65535
|
||||||
func (bf BloomFilter) Hash(msg []byte) []int {
|
func (bf *BloomFilter) Hash(msg []byte) []int {
|
||||||
hash := sha256.Sum256(msg)
|
hash := sha256.Sum256(msg)
|
||||||
|
|
||||||
pos1a := (int(hash[0]) + int(hash[1]) + int(hash[2]) + int(hash[3])) % 0xFF
|
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
|
// 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)
|
// (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)
|
pos := bf.Hash(msg)
|
||||||
if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] {
|
if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package primitives
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Identity is an encapsulation of Name, PrivateKey and other features
|
||||||
|
// that make up a Tapir client.
|
||||||
|
// The purpose of Identity is to prevent other classes directly accessing private key
|
||||||
|
// and to ensure the integrity of security-critical functions.
|
||||||
|
type Identity struct {
|
||||||
|
Name string
|
||||||
|
edpk *ed25519.PrivateKey
|
||||||
|
edpubk *ed25519.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeIdentity is a courtesy function for initializing a V3 Identity in-code.
|
||||||
|
func InitializeIdentity(name string, pk *ed25519.PrivateKey, pubk *ed25519.PublicKey) Identity {
|
||||||
|
return Identity{name, pk, pubk}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeEphemeralIdentity generates a new ephemeral identity, the private key of this identity is provided in the response.
|
||||||
|
func InitializeEphemeralIdentity() (Identity, ed25519.PrivateKey) {
|
||||||
|
epk, esk, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
|
ephemeralPublicKey := ed25519.PublicKey(epk)
|
||||||
|
ephemeralPrivateKey := ed25519.PrivateKey(esk)
|
||||||
|
ephemeralIdentity := InitializeIdentity("", &ephemeralPrivateKey, &ephemeralPublicKey)
|
||||||
|
return ephemeralIdentity, ephemeralPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyBytes returns the public key associated with this Identity in serializable-friendly
|
||||||
|
// format.
|
||||||
|
func (i *Identity) PublicKeyBytes() []byte {
|
||||||
|
return *i.edpubk
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey returns the public key associated with this Identity
|
||||||
|
func (i *Identity) PublicKey() ed25519.PublicKey {
|
||||||
|
return *i.edpubk
|
||||||
|
}
|
||||||
|
|
||||||
|
// EDH performs a diffie-helman operation on this identities private key with the given public key.
|
||||||
|
func (i *Identity) EDH(key ed25519.PublicKey) []byte {
|
||||||
|
secret := utils.EDH(*i.edpk, key)
|
||||||
|
return secret[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname provides the onion address associated with this Identity.
|
||||||
|
func (i *Identity) Hostname() string {
|
||||||
|
return utils.GetTorV3Hostname(*i.edpubk)
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ type TimeProvider interface {
|
||||||
type OSTimeProvider struct {
|
type OSTimeProvider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCurrentTime returns the time provided by the OS
|
||||||
func (ostp OSTimeProvider) GetCurrentTime() time.Time {
|
func (ostp OSTimeProvider) GetCurrentTime() time.Time {
|
||||||
return time.Now()
|
return time.Now()
|
||||||
}
|
}
|
||||||
|
|
118
service.go
118
service.go
|
@ -2,9 +2,9 @@ package tapir
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
"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/log"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
@ -15,73 +15,117 @@ import (
|
||||||
|
|
||||||
// Service defines the interface for a Tapir Service
|
// Service defines the interface for a Tapir Service
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity identity.Identity)
|
Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity *primitives.Identity)
|
||||||
Connect(hostname string, application Application) (string, error)
|
Connect(hostname string, application Application) (bool, error)
|
||||||
Listen(application Application) error
|
Listen(application Application) error
|
||||||
GetConnection(connectionID string) (*Connection, error)
|
GetConnection(connectionID string) (Connection, error)
|
||||||
WaitForCapabilityOrClose(connectionID string, capability string) (*Connection, error)
|
WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error)
|
||||||
Shutdown()
|
Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection Interface
|
||||||
|
type Connection interface {
|
||||||
|
Hostname() string
|
||||||
|
IsOutbound() bool
|
||||||
|
ID() *primitives.Identity
|
||||||
|
Expect() []byte
|
||||||
|
SetHostname(hostname string)
|
||||||
|
HasCapability(name string) bool
|
||||||
|
SetCapability(name string)
|
||||||
|
SetEncryptionKey(key [32]byte)
|
||||||
|
Send(message []byte)
|
||||||
|
Close()
|
||||||
|
App() Application
|
||||||
|
IsClosed() bool
|
||||||
|
}
|
||||||
|
|
||||||
// Connection defines a Tapir Connection
|
// Connection defines a Tapir Connection
|
||||||
type Connection struct {
|
type connection struct {
|
||||||
Hostname string
|
hostname string
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
capabilities sync.Map
|
capabilities sync.Map
|
||||||
encrypted bool
|
encrypted bool
|
||||||
key [32]byte
|
key [32]byte
|
||||||
App Application
|
app Application
|
||||||
ID identity.Identity
|
identity *primitives.Identity
|
||||||
Outbound bool
|
outbound bool
|
||||||
Closed bool
|
closed bool
|
||||||
MaxLength int
|
MaxLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConnection creates a new Connection
|
// NewConnection creates a new Connection
|
||||||
func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) *Connection {
|
func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection {
|
||||||
connection := new(Connection)
|
connection := new(connection)
|
||||||
connection.Hostname = hostname
|
connection.hostname = hostname
|
||||||
connection.conn = conn
|
connection.conn = conn
|
||||||
connection.App = app
|
connection.app = app
|
||||||
connection.ID = id
|
connection.identity = id
|
||||||
connection.Outbound = outbound
|
connection.outbound = outbound
|
||||||
connection.MaxLength = 1024
|
connection.MaxLength = 1024
|
||||||
go connection.App.Init(connection)
|
go connection.app.Init(connection)
|
||||||
return connection
|
return connection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID returns an identity.Identity encapsulation (for the purposes of cryptographic protocols)
|
||||||
|
func (c *connection) ID() *primitives.Identity {
|
||||||
|
return c.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
// App returns the overarching application using this Connection.
|
||||||
|
func (c *connection) App() Application {
|
||||||
|
return c.app
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// SetHostname sets the hostname on the connection
|
||||||
func (c *Connection) SetHostname(hostname string) {
|
func (c *connection) SetHostname(hostname string) {
|
||||||
log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.ID.Hostname(), c.Hostname, hostname)
|
log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.identity.Hostname(), c.hostname, hostname)
|
||||||
c.Hostname = hostname
|
c.hostname = hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCapability sets a capability on the connection
|
// SetCapability sets a capability on the connection
|
||||||
func (c *Connection) SetCapability(name string) {
|
func (c *connection) SetCapability(name string) {
|
||||||
log.Debugf("[%v -- %v] Setting Capability %v", c.ID.Hostname(), c.Hostname, name)
|
log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name)
|
||||||
c.capabilities.Store(name, true)
|
c.capabilities.Store(name, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasCapability checks if the connection has a given capability
|
// 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)
|
_, ok := c.capabilities.Load(name)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close forcibly closes the connection
|
// Close forcibly closes the connection
|
||||||
func (c *Connection) Close() {
|
func (c *connection) Close() {
|
||||||
|
c.closed = true
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expect blocks and reads a single Tapir packet , from the connection.
|
// 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)
|
buffer := make([]byte, c.MaxLength)
|
||||||
n, err := io.ReadFull(c.conn, buffer)
|
n, err := io.ReadFull(c.conn, buffer)
|
||||||
|
|
||||||
if n != c.MaxLength || err != nil {
|
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.conn.Close()
|
||||||
c.Closed = true
|
c.closed = true
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,25 +136,25 @@ func (c *Connection) Expect() []byte {
|
||||||
if ok {
|
if ok {
|
||||||
copy(buffer, decrypted)
|
copy(buffer, decrypted)
|
||||||
} else {
|
} 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.conn.Close()
|
||||||
c.Closed = true
|
c.closed = true
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
len, _ := binary.Uvarint(buffer[0:2])
|
len, _ := binary.Uvarint(buffer[0:2])
|
||||||
//log.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer)
|
//cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer)
|
||||||
return buffer[2 : len+2]
|
return buffer[2 : len+2]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEncryptionKey turns on application-level encryption on the connection using the given key.
|
// 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.key = key
|
||||||
c.encrypted = true
|
c.encrypted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send writes a given message to a Tapir packet (of 1024 bytes in length).
|
// 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)
|
buffer := make([]byte, c.MaxLength)
|
||||||
binary.PutUvarint(buffer[0:2], uint64(len(message)))
|
binary.PutUvarint(buffer[0:2], uint64(len(message)))
|
||||||
|
@ -119,18 +163,18 @@ func (c *Connection) Send(message []byte) {
|
||||||
if c.encrypted {
|
if c.encrypted {
|
||||||
var nonce [24]byte
|
var nonce [24]byte
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||||
// TODO: Surface is Error
|
log.Errorf("Could not read sufficient randomness %v. Closing connection", err)
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
c.Closed = true
|
c.closed = true
|
||||||
}
|
}
|
||||||
// MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag.
|
// MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag.
|
||||||
encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key)
|
encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key)
|
||||||
copy(buffer, encrypted[0:c.MaxLength])
|
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)
|
_, err := c.conn.Write(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
c.Closed = true
|
c.closed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
@ -1,16 +1,17 @@
|
||||||
package main
|
package testing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"cwtch.im/tapir"
|
"cwtch.im/tapir"
|
||||||
"cwtch.im/tapir/applications"
|
"cwtch.im/tapir/applications"
|
||||||
"cwtch.im/tapir/networks/tor"
|
"cwtch.im/tapir/networks/tor"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
"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/log"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
"os"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ func (ea SimpleApp) NewInstance() tapir.Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init is run when the connection is first started.
|
// 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
|
// First run the Authentication App
|
||||||
ea.AuthApp.Init(connection)
|
ea.AuthApp.Init(connection)
|
||||||
|
|
||||||
|
@ -37,61 +38,82 @@ func (ea SimpleApp) Init(connection *tapir.Connection) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AuthSuccess = false
|
||||||
|
|
||||||
// CheckConnection is a simple test that GetConnection is working.
|
// CheckConnection is a simple test that GetConnection is working.
|
||||||
func CheckConnection(service tapir.Service, hostname string) {
|
func CheckConnection(service tapir.Service, hostname string, group *sync.WaitGroup) {
|
||||||
for {
|
for {
|
||||||
_, err := service.GetConnection(hostname)
|
_, err := service.GetConnection(hostname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Infof("Authed!")
|
log.Infof("Authed!")
|
||||||
|
group.Done()
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
log.Errorf("Error %v", err)
|
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
log.Infof("Waiting for Authentication...%v", err)
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func TestTapir(t *testing.T) {
|
||||||
|
|
||||||
|
numRoutinesStart := runtime.NumGoroutine()
|
||||||
log.SetLevel(log.LevelDebug)
|
log.SetLevel(log.LevelDebug)
|
||||||
|
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
|
||||||
// Connect to Tor
|
// Connect to Tor
|
||||||
var acn connectivity.ACN
|
var acn connectivity.ACN
|
||||||
acn, _ = connectivity.StartTor("./", "")
|
acn, _ = connectivity.StartTor("./", "")
|
||||||
acn.WaitTillBootstrapped()
|
acn.WaitTillBootstrapped()
|
||||||
|
|
||||||
// Generate Server Keys
|
// Generate Server Keys
|
||||||
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
|
id, sk := primitives.InitializeEphemeralIdentity()
|
||||||
sk := ed25519.PrivateKey(privateKey)
|
|
||||||
pk := ed25519.PublicKey(pubkey)
|
|
||||||
id := identity.InitializeV3("server", &sk, &pk)
|
|
||||||
|
|
||||||
// Init a Client to Connect to the Server
|
|
||||||
client, clienthostname := genclient(acn)
|
|
||||||
go connectclient(client, pubkey)
|
|
||||||
|
|
||||||
// Init the Server running the Simple App.
|
// Init the Server running the Simple App.
|
||||||
var service tapir.Service
|
var service tapir.Service
|
||||||
service = new(tor.BaseOnionService)
|
service = new(tor.BaseOnionService)
|
||||||
service.Init(acn, sk, id)
|
service.Init(acn, sk, &id)
|
||||||
go CheckConnection(service, clienthostname)
|
|
||||||
service.Listen(SimpleApp{})
|
// Goroutine Management
|
||||||
|
sg := new(sync.WaitGroup)
|
||||||
|
sg.Add(1)
|
||||||
|
go func() {
|
||||||
|
service.Listen(SimpleApp{})
|
||||||
|
sg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for server to come online
|
||||||
|
time.Sleep(time.Second * 30)
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
wg.Add(2)
|
||||||
|
// Init a Client to Connect to the Server
|
||||||
|
client, clienthostname := genclient(acn)
|
||||||
|
go connectclient(client, id.PublicKey(), wg)
|
||||||
|
CheckConnection(service, clienthostname, wg)
|
||||||
|
wg.Wait()
|
||||||
|
// Wait for Server to Sync
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
log.Infof("Closing ACN...")
|
||||||
|
acn.Close()
|
||||||
|
sg.Wait()
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine())
|
||||||
|
if numRoutinesStart != runtime.NumGoroutine() {
|
||||||
|
t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine())
|
||||||
|
}
|
||||||
|
if !AuthSuccess {
|
||||||
|
t.Fatalf("Integration Test FAILED, client did not auth with server")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func genclient(acn connectivity.ACN) (tapir.Service, string) {
|
func genclient(acn connectivity.ACN) (tapir.Service, string) {
|
||||||
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
|
id, sk := primitives.InitializeEphemeralIdentity()
|
||||||
sk := ed25519.PrivateKey(privateKey)
|
|
||||||
pk := ed25519.PublicKey(pubkey)
|
|
||||||
id := identity.InitializeV3("client", &sk, &pk)
|
|
||||||
var client tapir.Service
|
var client tapir.Service
|
||||||
client = new(tor.BaseOnionService)
|
client = new(tor.BaseOnionService)
|
||||||
client.Init(acn, sk, id)
|
client.Init(acn, sk, &id)
|
||||||
return client, utils.GetTorV3Hostname(pk)
|
return client, id.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client will Connect and launch it's own Echo App goroutine.
|
// Client will Connect and launch it's own Echo App goroutine.
|
||||||
func connectclient(client tapir.Service, key ed25519.PublicKey) {
|
func connectclient(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) {
|
||||||
|
|
||||||
client.Connect(utils.GetTorV3Hostname(key), SimpleApp{})
|
client.Connect(utils.GetTorV3Hostname(key), SimpleApp{})
|
||||||
|
|
||||||
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
|
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
|
||||||
|
@ -100,6 +122,6 @@ func connectclient(client tapir.Service, key ed25519.PublicKey) {
|
||||||
|
|
||||||
conn, _ := client.GetConnection(utils.GetTorV3Hostname(key))
|
conn, _ := client.GetConnection(utils.GetTorV3Hostname(key))
|
||||||
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
|
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
|
||||||
|
AuthSuccess = true
|
||||||
os.Exit(0)
|
group.Done()
|
||||||
}
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/tapir"
|
||||||
|
"cwtch.im/tapir/applications"
|
||||||
|
"cwtch.im/tapir/networks/tor"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTapirMaliciousRemote(t *testing.T) {
|
||||||
|
|
||||||
|
numRoutinesStart := runtime.NumGoroutine()
|
||||||
|
log.SetLevel(log.LevelDebug)
|
||||||
|
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
|
||||||
|
// Connect to Tor
|
||||||
|
var acn connectivity.ACN
|
||||||
|
acn, _ = connectivity.StartTor("./", "")
|
||||||
|
acn.WaitTillBootstrapped()
|
||||||
|
|
||||||
|
// Generate Server Keys, not we generate two sets
|
||||||
|
id, _ := primitives.InitializeEphemeralIdentity()
|
||||||
|
id2, sk2 := primitives.InitializeEphemeralIdentity()
|
||||||
|
|
||||||
|
// Init the Server running the Simple App.
|
||||||
|
var service tapir.Service
|
||||||
|
service = new(tor.BaseOnionService)
|
||||||
|
// Initialize an onion service with one identity, but the auth app with another, this should
|
||||||
|
// trigger a failure in authentication protocol
|
||||||
|
service.Init(acn, sk2, &id)
|
||||||
|
|
||||||
|
// Goroutine Management
|
||||||
|
sg := new(sync.WaitGroup)
|
||||||
|
sg.Add(1)
|
||||||
|
go func() {
|
||||||
|
service.Listen(applications.AuthApp{})
|
||||||
|
sg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for server to come online
|
||||||
|
time.Sleep(time.Second * 30)
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
wg.Add(1)
|
||||||
|
// Init a Client to Connect to the Server
|
||||||
|
log.Infof("initializing the client....")
|
||||||
|
client, _ := genclient(acn)
|
||||||
|
go connectclientandfail(client, id2.PublicKey(), wg, t)
|
||||||
|
wg.Wait()
|
||||||
|
// Wait for Server to Sync
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
log.Infof("closing ACN...")
|
||||||
|
acn.Close()
|
||||||
|
sg.Wait()
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine())
|
||||||
|
if numRoutinesStart != runtime.NumGoroutine() {
|
||||||
|
t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client will Connect and launch it's own Echo App goroutine.
|
||||||
|
func connectclientandfail(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup, t *testing.T) {
|
||||||
|
client.Connect(utils.GetTorV3Hostname(key), applications.AuthApp{})
|
||||||
|
|
||||||
|
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
|
||||||
|
// we will wait a little while then exit.
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
|
||||||
|
log.Infof("Checking connection status...")
|
||||||
|
conn, err := client.GetConnection(utils.GetTorV3Hostname(key))
|
||||||
|
if err == nil {
|
||||||
|
group.Done()
|
||||||
|
t.Fatalf("Connection should have failed! %v %v", conn, err)
|
||||||
|
}
|
||||||
|
log.Infof("Successfully failed to authenticate...")
|
||||||
|
group.Done()
|
||||||
|
}
|
|
@ -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
|
Loading…
Reference in New Issue