forked from cwtch.im/tapir
Initial Commit
This commit is contained in:
commit
c769bdf208
|
@ -0,0 +1,3 @@
|
||||||
|
tor/
|
||||||
|
vendor/
|
||||||
|
.idea
|
|
@ -0,0 +1,7 @@
|
||||||
|
# TAPir: Tiny Anonymous Peer
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Work In Progress**
|
|
@ -0,0 +1,6 @@
|
||||||
|
package tapir
|
||||||
|
|
||||||
|
type Application interface {
|
||||||
|
NewInstance() Application
|
||||||
|
Init(connection *Connection)
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package tapir
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"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"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthApp struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthMessage struct {
|
||||||
|
LongTermPublicKey ed25519.PublicKey
|
||||||
|
EphemeralPublicKey ed25519.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ea AuthApp) NewInstance() Application {
|
||||||
|
return new(AuthApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ea AuthApp) Init(connection *Connection) {
|
||||||
|
longTermPubKey := ed25519.PublicKey(connection.ID.PublicKeyBytes())
|
||||||
|
epk, esk, _ := 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)
|
||||||
|
connection.Send(serialized)
|
||||||
|
message := connection.Expect()
|
||||||
|
|
||||||
|
var remoteAuthMessage AuthMessage
|
||||||
|
err := json.Unmarshal(message, &remoteAuthMessage)
|
||||||
|
if err != nil {
|
||||||
|
connection.conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3DH Handshake
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
// TODO: Replace this with proper transcript
|
||||||
|
challengeRemote, err := json.Marshal(remoteAuthMessage)
|
||||||
|
challengeLocal, err := json.Marshal(authMessage)
|
||||||
|
challenge := sha3.New512()
|
||||||
|
|
||||||
|
if connection.Outbound {
|
||||||
|
challenge.Write(challengeLocal)
|
||||||
|
challenge.Write(challengeRemote)
|
||||||
|
} else {
|
||||||
|
challenge.Write(challengeRemote)
|
||||||
|
challenge.Write(challengeLocal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key
|
||||||
|
// To test that the remote peer has done the same we calculate a challenge hash based on the transcript so far and send it to them
|
||||||
|
// We expect the remote to do the same, and compare the two.
|
||||||
|
// If successful we extend our auth capability to the connection and reassert the hostname.
|
||||||
|
challengeBytes := challenge.Sum([]byte{})
|
||||||
|
connection.Send(challengeBytes)
|
||||||
|
remoteChallenge := connection.Expect()
|
||||||
|
if subtle.ConstantTimeCompare(challengeBytes, remoteChallenge) == 1 {
|
||||||
|
connection.SetHostname(utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
|
||||||
|
connection.SetCapability("AUTH")
|
||||||
|
} else {
|
||||||
|
log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"cwtch.im/tapir"
|
||||||
|
"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"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Simple App is a trivial implementation of a basic p2p application
|
||||||
|
type SimpleApp struct {
|
||||||
|
tapir.AuthApp
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance should always return a new instantiation of the application.
|
||||||
|
func (ea SimpleApp) NewInstance() tapir.Application {
|
||||||
|
return new(SimpleApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init is run when the connection is first started.
|
||||||
|
func (ea SimpleApp) Init(connection *tapir.Connection) {
|
||||||
|
// First run the Authentication App
|
||||||
|
ea.AuthApp.Init(connection)
|
||||||
|
|
||||||
|
if connection.HasCapability("AUTH") {
|
||||||
|
// The code for out simple application (We just send and receive "Hello"
|
||||||
|
connection.Send([]byte("Hello"))
|
||||||
|
message := connection.Expect()
|
||||||
|
log.Infof("Received: %q", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Start a Client to Connect to the Server
|
||||||
|
go client(acn, pubkey)
|
||||||
|
|
||||||
|
// Start the Server running the Echo App.
|
||||||
|
var service tapir.Service
|
||||||
|
service = new(tapir.BaseOnionService)
|
||||||
|
service.Start(acn, sk, id)
|
||||||
|
service.Listen(SimpleApp{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(tapir.BaseOnionService)
|
||||||
|
client.Start(acn, sk, id)
|
||||||
|
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
|
||||||
|
// we will wait a little while then exit.
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
module cwtch.im/osp
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.openprivacy.ca/openprivacy/libricochet-go v1.0.3
|
||||||
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
git.openprivacy.ca/openprivacy/libricochet-go v1.0.3 h1:LHnhK9hzkqMY+iEE3TZ0FjZsYal05YDiamKmxDOXuts=
|
||||||
|
git.openprivacy.ca/openprivacy/libricochet-go v1.0.3/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY=
|
||||||
|
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
|
||||||
|
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
|
||||||
|
github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g=
|
||||||
|
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
||||||
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
@ -0,0 +1,163 @@
|
||||||
|
package tapir
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Start(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity identity.Identity)
|
||||||
|
Connect(hostname string, application Application) error
|
||||||
|
Listen(application Application) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
hostname string
|
||||||
|
conn net.Conn
|
||||||
|
capabilities sync.Map
|
||||||
|
encrypted bool
|
||||||
|
key [32]byte
|
||||||
|
app Application
|
||||||
|
ID identity.Identity
|
||||||
|
Outbound bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
go connection.app.Init(connection)
|
||||||
|
return 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) SetCapability(name string) {
|
||||||
|
log.Debugf("[%v -- %v] Setting Capability %v", c.ID.Hostname(), c.hostname, name)
|
||||||
|
c.capabilities.Store(name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) HasCapability(name string) bool {
|
||||||
|
_, ok := c.capabilities.Load(name)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Expect() []byte {
|
||||||
|
buffer := make([]byte, 1024)
|
||||||
|
// TODO handle ERROR
|
||||||
|
io.ReadFull(c.conn, buffer)
|
||||||
|
//log.Debugf("[%v -> %v] Wire Receive: %x", c.hostname, c.ID.Hostname(), buffer)
|
||||||
|
|
||||||
|
if c.encrypted {
|
||||||
|
var decryptNonce [24]byte
|
||||||
|
copy(decryptNonce[:], buffer[:24])
|
||||||
|
decrypted, ok := secretbox.Open(nil, buffer[24:], &decryptNonce, &c.key)
|
||||||
|
if ok {
|
||||||
|
copy(buffer, decrypted)
|
||||||
|
} else {
|
||||||
|
log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.hostname, c.ID.Hostname())
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
len, _ := binary.Uvarint(buffer[0:2])
|
||||||
|
return buffer[2 : len+2]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) SetEncryptionKey(key [32]byte) {
|
||||||
|
c.key = key
|
||||||
|
c.encrypted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Send(message []byte) {
|
||||||
|
|
||||||
|
maxLength := 1024
|
||||||
|
buffer := make([]byte, maxLength)
|
||||||
|
binary.PutUvarint(buffer[0:2], uint64(len(message)))
|
||||||
|
copy(buffer[2:], message)
|
||||||
|
|
||||||
|
if c.encrypted {
|
||||||
|
var nonce [24]byte
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag.
|
||||||
|
encrypted := secretbox.Seal(nonce[:], buffer[0:maxLength-40], &nonce, &c.key)
|
||||||
|
copy(buffer, encrypted[0:1024])
|
||||||
|
}
|
||||||
|
//log.Debugf("[%v -> %v] Wire Send %x", c.ID.Hostname(), c.hostname, buffer)
|
||||||
|
c.conn.Write(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseOnionService struct {
|
||||||
|
connections sync.Map
|
||||||
|
acn connectivity.ACN
|
||||||
|
id identity.Identity
|
||||||
|
privateKey ed25519.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseOnionService) Start(acn connectivity.ACN, sk ed25519.PrivateKey, id identity.Identity) {
|
||||||
|
// run add onion
|
||||||
|
// get listen context
|
||||||
|
s.acn = acn
|
||||||
|
s.id = id
|
||||||
|
s.privateKey = sk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseOnionService) GetConnection(hostname string) error {
|
||||||
|
return errors.New("no connection found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseOnionService) Connect(hostname string, app Application) error {
|
||||||
|
// connects to a remote server
|
||||||
|
// spins off to a connection struct
|
||||||
|
log.Debugf("Connecting to %v", hostname)
|
||||||
|
conn, _, err := s.acn.Open(hostname)
|
||||||
|
if err == nil {
|
||||||
|
log.Debugf("Connected to %v", hostname)
|
||||||
|
s.connections.Store(hostname, NewConnection(s.id, hostname, true, conn, app.NewInstance()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debugf("Error connecting to %v %v", hostname, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseOnionService) Listen(app Application) error {
|
||||||
|
// accepts a new connection
|
||||||
|
// spins off to a connection struct
|
||||||
|
ls, err := s.acn.Listen(s.privateKey, 9878)
|
||||||
|
log.Debugf("Starting a service on %v ", ls.AddressFull())
|
||||||
|
if err == nil {
|
||||||
|
for {
|
||||||
|
conn, err := ls.Accept()
|
||||||
|
if err == nil {
|
||||||
|
id := make([]byte, 10)
|
||||||
|
rand.Read(id)
|
||||||
|
tempHostname := "unknown-" + base64.StdEncoding.EncodeToString(id)
|
||||||
|
log.Debugf("Accepted connection from %v", tempHostname)
|
||||||
|
s.connections.Store(tempHostname, NewConnection(s.id, tempHostname, false, conn, app.NewInstance()))
|
||||||
|
} else {
|
||||||
|
ls.Close()
|
||||||
|
log.Debugf("Error accepting connection %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("Error listening to connection %v", err)
|
||||||
|
return err
|
||||||
|
}
|
Loading…
Reference in New Issue