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) }