commit c769bdf208deeabefa562b592f398d5ca8ddebe8 Author: Sarah Jamie Lewis Date: Wed May 15 12:45:45 2019 -0700 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92fd040 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +tor/ +vendor/ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c13d2e --- /dev/null +++ b/README.md @@ -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** \ No newline at end of file diff --git a/application.go b/application.go new file mode 100644 index 0000000..2c39454 --- /dev/null +++ b/application.go @@ -0,0 +1,6 @@ +package tapir + +type Application interface { + NewInstance() Application + Init(connection *Connection) +} diff --git a/auth_app.go b/auth_app.go new file mode 100644 index 0000000..e879441 --- /dev/null +++ b/auth_app.go @@ -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) + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..b292bc5 --- /dev/null +++ b/cmd/main.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f003393 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26a0296 --- /dev/null +++ b/go.sum @@ -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= diff --git a/service.go b/service.go new file mode 100644 index 0000000..be6ccde --- /dev/null +++ b/service.go @@ -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 +}