From 92157bb3aec964da5344375e5bb8784ad1250497 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sat, 13 Dec 2014 18:00:02 -0500 Subject: [PATCH 01/36] Allow go test to work with Darwin, FreeBSD, and noTPM. --- client/erasure_gui.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 client/erasure_gui.go diff --git a/client/erasure_gui.go b/client/erasure_gui.go new file mode 100644 index 0000000..016fada --- /dev/null +++ b/client/erasure_gui.go @@ -0,0 +1,16 @@ +// +build !nogui,!linux + +// Any build options that posses their own createErasureStorage should be excluded above. + +package main + +import ( + "github.com/agl/pond/client/disk" +) + +func (c *guiClient) createErasureStorage(pw string, stateFile *disk.StateFile) error { + c.gui.Actions() <- UIState{uiStateErasureStorage} + c.gui.Signal() + + return c.client.createErasureStorage(pw, stateFile) +} From 29d5f5d16f523b0a113e786143fc4dacc8affa97 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 10 Nov 2014 16:35:30 +0100 Subject: [PATCH 02/36] Bash scripts to compile a .proto file using either goprotobuf or gogoprotobuf. Avoids screwing around with directories or editing files now. Find gogoprotobuf at https://code.google.com/p/gogoprotobuf/ --- gogoprotoc.sh | 8 ++++++++ goprotoc.sh | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100755 gogoprotoc.sh create mode 100755 goprotoc.sh diff --git a/gogoprotoc.sh b/gogoprotoc.sh new file mode 100755 index 0000000..dd6ab99 --- /dev/null +++ b/gogoprotoc.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# +# Find gogoprotobuf at https://code.google.com/p/gogoprotobuf/ +# +# Install gogoprotobuf using : +# go get code.google.com/p/gogoprotobuf/{proto,protoc-gen-gogo,gogoproto} +# +exec protoc --proto_path=$GOPATH/src:. --gogo_out=. $* diff --git a/goprotoc.sh b/goprotoc.sh new file mode 100755 index 0000000..630fe63 --- /dev/null +++ b/goprotoc.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +protoc --proto_path=$GOPATH/src:. --go_out=. $* +# +# We must replace : +# import protos "github.com/agl/pond/protos" +# by : import protos "github.com/agl/pond/protos/pond.pb" +# +perl -p -i~ -e 's/(import protos \"github.com\/agl\/pond\/protos)\/pond.pb\"/$1\"/' disk/client.pb.go + From 86218101c241d260e82bdc2a83895a5b28455d41 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Tue, 28 Oct 2014 11:20:03 -0400 Subject: [PATCH 03/36] Refactor contact listing process based upon changes in the suggest_contacts branch. --- client/cli.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/cli.go b/client/cli.go index 4a195b4..9acf6d8 100644 --- a/client/cli.go +++ b/client/cli.go @@ -978,19 +978,23 @@ func (c *cliClient) draftsSummary() (table cliTable) { return } -func (c *cliClient) contactsSummary() (table cliTable) { +func (c *cliClient) contactsSummaryRaw(title string, + filter func(*Contact) bool) (table cliTable) { if len(c.contacts) == 0 { return } table = cliTable{ - heading: "Contacts", + heading: title, rows: make([]cliRow, 0, len(c.contacts)), } contacts := c.client.contactsSorted() for _, contact := range contacts { + if !filter(contact) { + continue + } if contact.cliId == invalidCliId { contact.cliId = c.newCliId() } @@ -1012,6 +1016,10 @@ func (c *cliClient) contactsSummary() (table cliTable) { return } +func (c *cliClient) contactsSummary() cliTable { + return c.contactsSummaryRaw("Contacts", func(c *Contact) bool { return true }) +} + func (c *cliClient) showQueueState() { c.queueMutex.Lock() queueLength := len(c.queue) From dee201e9dbd6fc654f83fa900fd3f4a4afb38862 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Tue, 28 Oct 2014 20:20:12 -0400 Subject: [PATCH 04/36] Abstracted message editing from compose as inputTextBlock. This is useful for various planned features, including contat invitations, messages with multiple recipants, and editable notes fields for contacts. In fact, I considered moving inputTextBlock to cli-inout.go, but it wouldn't save many imports in cli.go to do so. --- client/cli.go | 76 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/client/cli.go b/client/cli.go index 9acf6d8..eeea6ac 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1646,28 +1646,11 @@ Handle: return } -func (c *cliClient) compose(to *Contact, draft *Draft, inReplyTo *InboxMessage) { - if draft == nil { - draft = &Draft{ - id: c.randId(), - created: time.Now(), - to: to.id, - cliId: c.newCliId(), - } - if inReplyTo != nil && inReplyTo.message != nil { - draft.inReplyTo = inReplyTo.message.GetId() - draft.body = indentForReply(inReplyTo.message.GetBody()) - } - c.Printf("%s Created new draft: %s%s%s\n", termInfoPrefix, termCliIdStart, draft.cliId.String(), termReset) - c.drafts[draft.id] = draft - c.setCurrentObject(draft) - } - if to == nil { - to = c.contacts[draft.to] - } - if to.isPending { - c.Printf("%s Cannot send message to pending contact\n", termErrPrefix) - return +func (c *cliClient) inputTextBlock(draft string, isMessage bool) (body string, ok bool) { + ok = false + predraft := map[bool]string{ + true: "# Pond message. Lines prior to the first blank line are ignored.\n", + false: "", } tempDir, err := system.SafeTempDir() @@ -1686,12 +1669,10 @@ func (c *cliClient) compose(to *Contact, draft *Draft, inReplyTo *InboxMessage) os.Remove(tempFileName) }() - fmt.Fprintf(tempFile, "# Pond message. Lines prior to the first blank line are ignored.\nTo: %s\n\n", to.name) - if len(draft.body) == 0 { - tempFile.WriteString("\n") - } else { - tempFile.WriteString(draft.body) + if len(draft) == 0 { + draft = "\n" } + fmt.Fprintf(tempFile, predraft[isMessage]+draft) // The editor is forced to vim because I'm not sure about leaks from // other editors. (I'm not sure about leaks from vim either, but at @@ -1717,10 +1698,45 @@ func (c *cliClient) compose(to *Contact, draft *Draft, inReplyTo *InboxMessage) return } - if i := bytes.Index(contents, []byte("\n\n")); i >= 0 { - contents = contents[i+2:] + if isMessage { + if i := bytes.Index(contents, []byte("\n\n")); i >= 0 { + contents = contents[i+2:] + } + } + body = string(contents) + ok = true + return +} + +func (c *cliClient) compose(to *Contact, draft *Draft, inReplyTo *InboxMessage) { + if draft == nil { + draft = &Draft{ + id: c.randId(), + created: time.Now(), + to: to.id, + cliId: c.newCliId(), + } + if inReplyTo != nil && inReplyTo.message != nil { + draft.inReplyTo = inReplyTo.message.GetId() + draft.body = indentForReply(inReplyTo.message.GetBody()) + } + c.Printf("%s Created new draft: %s%s%s\n", termInfoPrefix, termCliIdStart, draft.cliId.String(), termReset) + c.drafts[draft.id] = draft + c.setCurrentObject(draft) + } + if to == nil { + to = c.contacts[draft.to] + } + if to.isPending { + c.Printf("%s Cannot send message to pending contact\n", termErrPrefix) + return + } + + body, ok := c.inputTextBlock(fmt.Sprintf("To: %s\n\n"+draft.body, to.name), true) + if !ok { + return } - draft.body = string(contents) + draft.body = body c.printDraftSize(draft) c.save() From a84880ab9a5f303aa2b09779ec37a15f101fc52c Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Thu, 6 Nov 2014 12:25:52 +0100 Subject: [PATCH 05/36] Abstract beginPandaKeyExchange from the CLI's newContactCommand Rebased so newKeyExchange now outside beginPandaKeyExchange --- client/cli.go | 19 ++----------------- client/client.go | 25 +++++++++++++++++++++++++ client/gui.go | 16 ++-------------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/client/cli.go b/client/cli.go index eeea6ac..d1e4ea3 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1584,8 +1584,6 @@ Handle: cliId: c.newCliId(), } - c.newKeyExchange(contact) - stack := &panda.CardStack{ NumDecks: 1, } @@ -1594,21 +1592,8 @@ Handle: Cards: *stack, } - mp := c.newMeetingPlace() - - c.contacts[contact.id] = contact - kx, err := panda.NewKeyExchange(c.rand, mp, &secret, contact.kxsBytes) - if err != nil { - panic(err) - } - kx.Testing = c.testing - contact.pandaKeyExchange = kx.Marshal() - contact.kxsBytes = nil - - c.save() - c.pandaWaitGroup.Add(1) - contact.pandaShutdownChan = make(chan struct{}) - go c.runPANDA(contact.pandaKeyExchange, contact.id, contact.name, contact.pandaShutdownChan) + c.newKeyExchange(contact) + c.beginPandaKeyExchange(contact, secret) c.Printf("%s Key exchange running in background.\n", termPrefix) case renameCommand: diff --git a/client/client.go b/client/client.go index 0d5ddef..57ba2c9 100644 --- a/client/client.go +++ b/client/client.go @@ -1290,6 +1290,31 @@ func (c *client) runPANDA(serialisedKeyExchange []byte, id uint64, name string, } } +// Launches a runPANDA goroutine based upon a panda.SharedSecret and a +// preliminary contact struct. +func (c *client) beginPandaKeyExchange(contact *Contact, secret panda.SharedSecret) { + if _, ok := c.contactByName(contact.name); ok { + c.log.Printf("A contact by the name %s already exists, this is an internal error.", contact.name) + return + } + + mp := c.newMeetingPlace() + + c.contacts[contact.id] = contact + kx, err := panda.NewKeyExchange(c.rand, mp, &secret, contact.kxsBytes) + if err != nil { + panic(err) + } + kx.Testing = c.testing + contact.pandaKeyExchange = kx.Marshal() + contact.kxsBytes = nil + + c.save() + c.pandaWaitGroup.Add(1) + contact.pandaShutdownChan = make(chan struct{}) + go c.runPANDA(contact.pandaKeyExchange, contact.id, contact.name, contact.pandaShutdownChan) +} + // processPANDAUpdate runs on the main client goroutine and handles messages // from a runPANDA goroutine. func (c *client) processPANDAUpdate(update pandaUpdate) { diff --git a/client/gui.go b/client/gui.go index 7d96a5f..0bf1bd1 100644 --- a/client/gui.go +++ b/client/gui.go @@ -2777,19 +2777,11 @@ SharedSecretEvent: secret.Hours = click.spinButtons["hour"] secret.Minutes = click.spinButtons["minute"] } - mp := c.newMeetingPlace() - c.contacts[contact.id] = contact + // c.newKeyExchange(contact) was run earlier + c.beginPandaKeyExchange(contact, secret) c.contactsUI.Add(contact.id, contact.name, "pending", indicatorNone) c.contactsUI.Select(contact.id) - - kx, err := panda.NewKeyExchange(c.rand, mp, &secret, contact.kxsBytes) - if err != nil { - panic(err) - } - kx.Testing = c.testing - contact.pandaKeyExchange = kx.Marshal() - contact.kxsBytes = nil break SharedSecretEvent case click.name == "generate": c.gui.Actions() <- SetEntry{name: "shared", text: panda.NewSecretString(c.rand)} @@ -2798,10 +2790,6 @@ SharedSecretEvent: } } - c.save() - c.pandaWaitGroup.Add(1) - contact.pandaShutdownChan = make(chan struct{}) - go c.runPANDA(contact.pandaKeyExchange, contact.id, contact.name, contact.pandaShutdownChan) return c.showContact(contact.id) } From cbec730a456dd581f8ce7e4138a55bcedd2f23cc Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Thu, 6 Nov 2014 12:33:13 +0100 Subject: [PATCH 06/36] Added interface independent introduction routines These create and parse the pond-introduce-panda urls Rebased so newKeyExchange now outside beginPandaKeyExchange Also rebased to use a counter rather than a random character, but maybe it's worth abstracting the cliId code for this. --- client/client.go | 13 +++ client/introduce.go | 194 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 client/introduce.go diff --git a/client/client.go b/client/client.go index 57ba2c9..cbd89a8 100644 --- a/client/client.go +++ b/client/client.go @@ -356,6 +356,19 @@ NextChar: return } +func hexDecodeSafe(dst []byte, src string) bool { + l := len(dst) // amazingly this actually works if you call using [:] + if hex.DecodedLen(len(src)) != l { + return false + } + s := []byte(src) + n, err := hex.Decode(dst, s) + if err != nil || n != l { + return false + } + return true +} + // InboxMessage represents a message in the client's inbox. (Acks also appear // as InboxMessages, but their message.Body is empty.) type InboxMessage struct { diff --git a/client/introduce.go b/client/introduce.go new file mode 100644 index 0000000..df2771b --- /dev/null +++ b/client/introduce.go @@ -0,0 +1,194 @@ +package main + +import ( + "fmt" + "net/url" + "regexp" + + "github.com/agl/pond/panda" +) + +const ( + introducePandaMessageDesc = "Introduction URLs for proposed new contacts :\n" +) + +func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact) (string, string) { + panda_secret := panda.NewSecretString(c.rand)[2:] + s := func(cnt *Contact) string { + return fmt.Sprintf("pond-introduce-panda://%s/%s/%x/\n", + url.QueryEscape(cnt.name), panda_secret, + cnt.theirIdentityPublic) // no EncodeToString? + } + return s(cnt1), s(cnt2) +} + +func (c *client) introducePandaMessages_onemany(cnts contactList) []string { + var urls []string = make([]string, len(cnts)) + cnt1 := cnts[0] + for i, cnt2 := range cnts[1:] { + // if i==0 { continue } + u1, u2 := c.introducePandaMessages_pair(cnt1, cnt2) + urls[0] += u1 + urls[i] = u2 + } + return urls +} + +func (c *client) introducePandaMessages_group(cnts contactList) []string { + n := len(cnts) + var urls []string = make([]string, len(cnts)) + // for i := 0; i < n; i++ { urls[i] = "" } + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + ui, uj := c.introducePandaMessages_pair(cnts[i], cnts[j]) + urls[i] += ui + urls[j] += uj + } + } + return urls +} + +func (c *client) introducePandaMessages_fancy(shown, hidden contactList) ([]string, []string) { + n := len(shown) + len(hidden) + var urls []string = make([]string, n) + cnts := append(shown, hidden...) + for i := 0; i < len(shown); i++ { + for j := i + 1; j < n; j++ { + ui, uj := c.introducePandaMessages_pair(cnts[i], cnts[j]) + urls[i] += ui + urls[j] += uj + } + } + return urls[0:len(shown)], urls[len(shown):] +} + +// func introducePandaMessages_onemany(cnts contactList) ([]string) { +// urls1,urls2 := introducePandaMessages_fancy({cnts[0]},cnts[1:]) +// return append(urls1,urls2...) +// } + +// func introducePandaMessages_group(cnts contactList) ([]string) { +// urls,_ := introducePandaMessages_fancy(cnts,nil) +// return urls +// } + +type ProposedContact struct { + sharedSecret string + theirIdentityPublic [32]byte + name string + id uint64 // zero if new or failed +} + +func (c *client) checkProposedContactName(sender uint64, pc ProposedContact) { + // We should consider using JaroWinkler or Levenshtein from + // "github.com/antzucaro/matchr" here : + // https://godoc.org/github.com/antzucaro/matchr#JaroWinkler + // Or maybe a fast fuzzy spelling suggestion algorithm + // https://github.com/sajari/fuzzy + // for _, contact := range c.contacts { } + // At least we now alphabatize the contacts listing however. + s := "" + _, ok := c.contactByName(pc.name) + if !ok { + return + } + i := 0 + for { + s = fmt.Sprintf("/%d?", i) + _, ok := c.contactByName(pc.name + s) + if !ok { + break + } + i++ + } + c.log.Printf("Another contact is already named %s, appending '%s'. Rename them, but make sure %s hasn't done anything nefarious here.", + pc.name, s, c.contacts[sender].name) + pc.name += s + + // if userlog { + // e := fmt.Sprintf("%s suggested the name %s for %s. Verify that nothing nefarious happened and rename them if desired.", + // c.contacts[sender].name,pc.name,c.contacts[id1].name); + // c.logEvent(c.contacts[id1],e) + // c.logEvent(c.contacts[sender],e) + // } // We need to be able to logEvent to the proposed contact here too. +} + +// Finds and parses all the pond-introduce-panda URLs in a message body. +// Returns a list of ProposedContacts from which to create add contact buttons. +func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContact { + var l []ProposedContact + re := regexp.MustCompile("(pond-introduce-panda)://([^/]+)/([^/]+)/([0-9A-Fa-f]{64})/") + ms := re.FindAllStringSubmatch(body, -1) // -1 means find all + const ( + urlparse_protocol = 1 + urlparse_name = 2 + urlparse_sharedSecret = 3 + urlparse_theirIdentityPublic = 4 + ) + for _, m := range ms { + if !panda.IsAcceptableSecretString(m[urlparse_sharedSecret]) { + c.log.Printf("Unacceptably weak secret '%s' for %s.", + m[urlparse_sharedSecret], m[urlparse_name]) + } + var pc ProposedContact + pc.sharedSecret = m[urlparse_sharedSecret] + if !hexDecodeSafe(pc.theirIdentityPublic[:], m[urlparse_theirIdentityPublic]) { + c.log.Printf("Bad public identity %s, skipping.", m[urlparse_theirIdentityPublic]) + continue + } + n, err := url.QueryUnescape(m[urlparse_name]) + if err != nil { + c.log.Printf("Badly escaped name %s, fix using rename.", m[urlparse_name]) + } else { + pc.name = n + } + l = append(l, pc) + // We allow contacts to be added even if they fail these checks because + // maybe they're the legit contact and the existing one is bad. + c.checkProposedContactName(sender, pc) + } + return l +} + +func (c *client) parsePandaURLs(msg *InboxMessage) []ProposedContact { + var body string + // msg.message could be nil if we're in a half paired message situation + if msg.message != nil { + body = string(msg.message.Body) + } + return c.parsePandaURLsText(msg.from, body) +} + +// Add a ProposedContact using PANDA once by building panda.SharedSecret and +// the basic contact struct to call beginPandaKeyExchange. +func (c *client) beginProposedPandaKeyExchange(pc ProposedContact) *Contact { + if len(pc.sharedSecret) == 0 || !panda.IsAcceptableSecretString(pc.sharedSecret) { + c.log.Printf("Unacceptably weak secret '%s'.", pc.sharedSecret) + return nil + } + if pc.id != 0 { + c.log.Printf("Attempted to add introduced contact %s, who is your existing contact %s, this is an internal error.\n", termPrefix, + pc.name, c.contacts[pc.id].name) + return nil + } + + contact := &Contact{ + name: pc.name, + isPending: true, + id: c.randId(), + theirIdentityPublic: pc.theirIdentityPublic, + } + // theirIdentityPublic set distinguishes contacts pending by introduction + // copy(contact.theirIdentityPublic[:], pc.theirIdentityPublic[:]) + + stack := &panda.CardStack{ + NumDecks: 1, + } + secret := panda.SharedSecret{ + Secret: pc.sharedSecret, + Cards: *stack, + } + c.newKeyExchange(contact) + c.beginPandaKeyExchange(contact, secret) + return contact +} From 0659fb933b703b690cc93541da7996ce56201d1e Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 10 Nov 2014 17:38:48 +0100 Subject: [PATCH 07/36] Abstract newDraft from the CLI's compose routine --- client/cli.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/client/cli.go b/client/cli.go index d1e4ea3..4e9f6f9 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1693,20 +1693,25 @@ func (c *cliClient) inputTextBlock(draft string, isMessage bool) (body string, o return } +func (c *client) newDraft(to *Contact, inReplyTo *InboxMessage) *Draft { + draft := &Draft{ + id: c.randId(), + created: time.Now(), + to: to.id, + } + if inReplyTo != nil && inReplyTo.message != nil { + draft.inReplyTo = inReplyTo.message.GetId() + draft.body = indentForReply(inReplyTo.message.GetBody()) + } + c.drafts[draft.id] = draft + return draft +} + func (c *cliClient) compose(to *Contact, draft *Draft, inReplyTo *InboxMessage) { if draft == nil { - draft = &Draft{ - id: c.randId(), - created: time.Now(), - to: to.id, - cliId: c.newCliId(), - } - if inReplyTo != nil && inReplyTo.message != nil { - draft.inReplyTo = inReplyTo.message.GetId() - draft.body = indentForReply(inReplyTo.message.GetBody()) - } + draft = c.newDraft(to, inReplyTo) + draft.cliId = c.newCliId() c.Printf("%s Created new draft: %s%s%s\n", termInfoPrefix, termCliIdStart, draft.cliId.String(), termReset) - c.drafts[draft.id] = draft c.setCurrentObject(draft) } if to == nil { From 2485be31cc317fde46d038590e43253aaada75cc Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Tue, 28 Oct 2014 11:09:02 -0400 Subject: [PATCH 08/36] Preliminary introducion functionality for the CLI --- client/cli-input.go | 10 +++ client/cli.go | 168 ++++++++++++++++++++++++++++++++++++++++++++ client/client.go | 9 +++ 3 files changed, 187 insertions(+) diff --git a/client/cli-input.go b/client/cli-input.go index 291c624..c2e0dfa 100644 --- a/client/cli-input.go +++ b/client/cli-input.go @@ -48,6 +48,9 @@ var cliCommands = []cliCommand{ {"inbox", showInboxSummaryCommand{}, "Show the Inbox", 0}, {"log", logCommand{}, "Show recent log entries", 0}, {"new-contact", newContactCommand{}, "Start a key exchange with a new contact", 0}, + {"introduce", introduceContactCommand{}, "Introduce a contact to multiple contacts", contextContact}, + {"introgroup", introduceContactGroupCommand{}, "Introduce a group of contacts to one another", 0}, + {"greet", greetContactCommand{}, "Accept an introduction of a proposed new contact", contextInbox}, {"outbox", showOutboxSummaryCommand{}, "Show the Outbox", 0}, {"queue", showQueueStateCommand{}, "Show the queue", 0}, {"quit", quitCommand{}, "Exit Pond", 0}, @@ -92,6 +95,13 @@ type newContactCommand struct { Name string } +type introduceContactCommand struct{} +type introduceContactGroupCommand struct{} + +type greetContactCommand struct { + Index string +} + type renameCommand struct { NewName string } diff --git a/client/cli.go b/client/cli.go index 4e9f6f9..327306d 100644 --- a/client/cli.go +++ b/client/cli.go @@ -11,6 +11,7 @@ import ( "os/signal" "path/filepath" "strconv" + "strings" "sync" "syscall" "time" @@ -1603,6 +1604,103 @@ Handle: c.Printf("%s Select contact first\n", termWarnPrefix) } + case introduceContactCommand: + contact, ok := c.currentObj.(*Contact) + if !ok { + c.Printf("%s Select contact first\n", termWarnPrefix) + return + } + + cl := c.inputContactList("Introduce "+contact.name+" to contacts : ", + func(cnt *Contact) bool { return !cnt.isPending && contact.id != cnt.id }) + if len(cl) == 0 { + return + } + cl = append(contactList{contact}, cl...) + + // Build from notes eventually + prebody0 := "To: " + cl[1].name + for _, to := range cl[2:] { + prebody0 += ", " + to.name + } + prebody0 += "\n\n" + body0, ok := c.inputTextBlock(prebody0, true) + if !ok { + c.Printf("Not OK, what now?") + } + bodyn, ok := c.inputTextBlock("To: "+cl[0].name+"\n\n", true) + if !ok { + c.Printf("Not OK, what now?") + } + + // c.introduceContact_onemany(contact,cl) + urls := c.introducePandaMessages_onemany(cl) + for i := range cl { + draft := c.newDraft(cl[i], nil) + draft.cliId = c.newCliId() + if i == 0 { + draft.body = body0 + } else { + draft.body = bodyn + } + draft.body += introducePandaMessageDesc + urls[i] + c.sendDraft(draft) + c.Printf("%s Sending introduction message %s%s%s to %s\n", termInfoPrefix, + termCliIdStart, draft.cliId.String(), termReset, cl[i].name) + } + c.save() + + case introduceContactGroupCommand: + cl := c.inputContactList("Introduce contacts to one another.", + func(cnt *Contact) bool { return !cnt.isPending }) + if len(cl) == 0 { + return + } + + prebody := "To: " + cl[1].name + for _, to := range cl[2:] { + prebody += ", " + to.name + } + prebody += "\n\n" + body, ok := c.inputTextBlock(prebody, true) + if !ok { + c.Printf("Not OK, what now?") + } + + urls := c.introducePandaMessages_group(cl) + for i := range cl { + draft := c.newDraft(cl[i], nil) + draft.cliId = c.newCliId() + draft.body = body + introducePandaMessageDesc + urls[i] + c.sendDraft(draft) + c.Printf("%s Sending introduction message %s%s%s to %s\n", termInfoPrefix, + termCliIdStart, draft.cliId.String(), termReset, cl[i].name) + } + c.save() + + case greetContactCommand: + msg, ok := c.currentObj.(*InboxMessage) + if !ok { + c.Printf("%s Select inbox message first\n", termWarnPrefix) + return + } + + pcs := c.parsePandaURLs(msg) + for i, pc := range pcs { + if cmd.Index == "*" || cmd.Index == pc.name || + cmd.Index == fmt.Sprintf("%d", i) { + if pc.id != 0 { + c.Printf("%s Introduced contact %s is your existing contact %s\n", termPrefix, pc.name, c.contacts[pc.id].name) + return + } + c.Printf("%s Begining PANDA key exchange with %s\n", termPrefix, pc.name) + c.beginProposedPandaKeyExchange(pc) + if cmd.Index != "*" { + return + } + } + } + case retainCommand: msg, ok := c.currentObj.(*InboxMessage) if !ok { @@ -1769,6 +1867,26 @@ func (c *cliClient) showInbox(msg *InboxMessage) { c.Printf("\n") c.term.Write([]byte(terminalEscape(string(msgText), true /* line breaks ok */))) c.Printf("\n") + + pcs := c.parsePandaURLs(msg) + if len(pcs) > 0 { + c.Printf("%s Introduced contacts. Add with greet command.\n", termPrefix) + } + for i, pc := range pcs { + s := "" + if pc.id != 0 { + s0 := "exists" + s1 := "" + if c.contacts[pc.id].isPending { + s0 = "pending" + } + if c.contacts[pc.id].name != pc.name { + s1 += "as " + c.contacts[pc.id].name + } + s = fmt.Sprintf("(%s%s)", s0, s1) + } + c.Printf("%d. %s %s\n", i, pc.name, s) + } } func (c *cliClient) showOutbox(msg *queuedMessage) { @@ -1853,6 +1971,56 @@ func (c *cliClient) renameContact(contact *Contact, newName string) { c.save() } +func (c *cliClient) inputContactList(title string, + filter func(*Contact) bool) (cl contactList) { + c.contactsSummaryRaw(title, filter).WriteTo(c.term) + + var prefix string = "" + for { + c.term.SetPrompt(prefix + "contacts> ") + line, err := c.term.ReadLine() + if err != nil { + cl = nil // Empty an array with garbage cllection + return + } + xs := strings.Fields(line) + if len(xs) <= 0 { + return + } + for _, x := range xs { + id, ok := cliIdFromString(x) + if !ok { + c.Printf("%s Bad contact tag %s.\n", termWarnPrefix, x) + if len(cl) == 0 { + return + } + continue + } + contact := c.cliIdToContact(id) + if contact == nil { + c.Printf("%s Tag %s is not a contact.\n", termWarnPrefix, x) + if len(xs) != 1 && len(cl) == 0 { + return + } + continue + } + if !filter(contact) { + c.Printf("%s Contact %s not allowed\n", termErrPrefix, contact.name) + continue + } + c.Printf("%s Added %s \n", termPrefix, contact.name) + cl = append(cl, contact) + } + if prefix == "" { + if len(cl) > 1 { + return + } + c.Printf("%s Enter a blank line when done.\n", termPrefix) + prefix = "more " + } + } +} + func (c *cliClient) showContact(contact *Contact) { if len(contact.pandaResult) > 0 { c.Printf("%s PANDA error: %s\n", termErrPrefix, terminalEscape(contact.pandaResult, false)) diff --git a/client/client.go b/client/client.go index cbd89a8..7b82c6e 100644 --- a/client/client.go +++ b/client/client.go @@ -356,6 +356,15 @@ NextChar: return } +func (c *client) cliIdToContact(id cliId) *Contact { + for _, contact := range c.contacts { + if contact.cliId == id { + return contact + } + } + return nil +} + func hexDecodeSafe(dst []byte, src string) bool { l := len(dst) // amazingly this actually works if you call using [:] if hex.DecodedLen(len(src)) != l { From c777b35ff3d2eee98a1df4ee03376545738ff4f8 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 7 Nov 2014 17:11:13 +0100 Subject: [PATCH 09/36] Save theirIdentityPublic to the state file even if contact.isPending because we save their proposed identity there. --- client/disk.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/disk.go b/client/disk.go index 8406609..7252270 100644 --- a/client/disk.go +++ b/client/disk.go @@ -108,6 +108,14 @@ func (c *client) unmarshal(state *disk.State) error { if cont.IsPending != nil && *cont.IsPending { contact.isPending = true + } + + if len(cont.TheirIdentityPublic) != len(contact.theirIdentityPublic) && !contact.isPending { + return errors.New("client: contact missing identity public key") + } + copy(contact.theirIdentityPublic[:], cont.TheirIdentityPublic) + + if contact.isPending == true { continue } @@ -129,11 +137,6 @@ func (c *client) unmarshal(state *disk.State) error { } copy(contact.theirPub[:], cont.TheirPub) - if len(cont.TheirIdentityPublic) != len(contact.theirIdentityPublic) { - return errors.New("client: contact missing identity public key") - } - copy(contact.theirIdentityPublic[:], cont.TheirIdentityPublic) - copy(contact.theirLastDHPublic[:], cont.TheirLastPublic) copy(contact.theirCurrentDHPublic[:], cont.TheirCurrentPublic) @@ -268,6 +271,7 @@ func (c *client) marshal() []byte { PandaError: proto.String(contact.pandaResult), RevokedUs: proto.Bool(contact.revokedUs), } + cont.TheirIdentityPublic = contact.theirIdentityPublic[:] if !contact.isPending { cont.MyGroupKey = contact.myGroupKey.Marshal() cont.TheirGroup = contact.myGroupKey.Group.Marshal() @@ -275,7 +279,6 @@ func (c *client) marshal() []byte { cont.TheirPub = contact.theirPub[:] cont.Generation = proto.Uint32(contact.generation) - cont.TheirIdentityPublic = contact.theirIdentityPublic[:] cont.TheirLastPublic = contact.theirLastDHPublic[:] cont.TheirCurrentPublic = contact.theirCurrentDHPublic[:] } From d5112a2f63fd86f2ad505ae72d6424c28a511e70 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Thu, 6 Nov 2014 17:06:20 +0100 Subject: [PATCH 10/36] Greet introduced contacts in GUI. Rebased with newKeyExchange now outside beginPandaKeyExchange --- client/gui.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/client/gui.go b/client/gui.go index 0bf1bd1..8926a58 100644 --- a/client/gui.go +++ b/client/gui.go @@ -1311,6 +1311,7 @@ func (c *guiClient) showInbox(id uint64) interface{} { // The UI names widgets with strings so these prefixes are used to // generate names for the dynamic parts of the UI. const ( + greetPrefix = "greet-" detachmentDecryptPrefix = "detachment-decrypt-" detachmentVBoxPrefix = "detachment-decrypt-" detachmentProgressPrefix = "detachment-progress-" @@ -1319,6 +1320,52 @@ func (c *guiClient) showInbox(id uint64) interface{} { attachmentPrefix = "attachment-" ) + pcs := c.parsePandaURLs(msg) + if len(pcs) > 0 { + grid := Grid{widgetBase: widgetBase{marginLeft: 25}, rowSpacing: 3} + + for i, pc := range pcs { + var greet string + if pc.id == 0 { + greet = "Greet" + } else { + cnt := c.contacts[pc.id] + if cnt.isPending { + greet = "Pending" + } else { + greet = "Exists" + if pc.name != cnt.name { + greet += " as " + cnt.name + } + } + // Should say Verified if the contact existed previously + } + grid.rows = append(grid.rows, []GridE{ + {1, 1, Label{ + widgetBase: widgetBase{vAlign: AlignCenter, hAlign: AlignStart}, + text: maybeTruncate(pc.name), + }}, + {1, 1, Button{ + widgetBase: widgetBase{ + name: fmt.Sprintf("%s%d", greetPrefix, i), + insensitive: greet != "Greet", + }, + text: greet, + }}, + }) + } + + c.gui.Actions() <- InsertRow{name: "lhs", pos: lhsNextRow, row: []GridE{ + {1, 1, Label{ + widgetBase: widgetBase{font: fontMainLabel, foreground: colorHeaderForeground, hAlign: AlignEnd, vAlign: AlignCenter}, + text: "INTRODUCTIONS", + }}, + }} + lhsNextRow++ + c.gui.Actions() <- InsertRow{name: "lhs", pos: lhsNextRow, row: []GridE{{2, 1, grid}}} + lhsNextRow++ + } + widgetForDetachmentProcess := func(index int) Widget { return VBox{ widgetBase: widgetBase{name: fmt.Sprintf("detachment-vbox-%d", index)}, @@ -1548,6 +1595,18 @@ NextEvent: continue } switch { + case strings.HasPrefix(click.name, greetPrefix): + i, ok := strconv.Atoi(click.name[len(greetPrefix):]) + if ok != nil || i >= len(pcs) { + panic("invalid greet command") + } + contact := c.beginProposedPandaKeyExchange(pcs[i]) + c.contactsUI.Add(contact.id, contact.name, "pending", indicatorNone) + c.contactsUI.Select(contact.id) + c.gui.Actions() <- Sensitive{name: click.name, sensitive: false} + c.gui.Actions() <- SetButtonText{name: click.name, text: "Pending"} + c.gui.Signal() + continue case strings.HasPrefix(click.name, attachmentPrefix): i, _ := strconv.Atoi(click.name[len(attachmentPrefix):]) c.gui.Actions() <- FileOpen{ From 336e6c99dc7891765036663fc8ae7c884fc6f5e3 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Thu, 27 Nov 2014 19:41:30 +0100 Subject: [PATCH 11/36] Introduction screen and sidebar button to GUI. Joint effort between Jeff Burdges , Alan Fay , and Ramnarayan Vedam --- client/gui.go | 231 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 230 insertions(+), 1 deletion(-) diff --git a/client/gui.go b/client/gui.go index 8926a58..aa4f44c 100644 --- a/client/gui.go +++ b/client/gui.go @@ -61,6 +61,7 @@ const ( uiStateMain uiStateCreateAccount uiStateCreatePassphrase + uiStateIntroduceContact uiStateNewContact uiStateNewContact2 uiStateShowContact @@ -142,7 +143,7 @@ func (c *guiClient) nextEvent(currentMsgId uint64) (event interface{}, wanted bo wanted = true } if click, ok := event.(Click); ok { - wanted = wanted || click.name == "newcontact" || click.name == "compose" + wanted = wanted || click.name == "newcontact" || click.name == "compose" || click.name == "introduce" } return } @@ -489,6 +490,10 @@ func (c *guiClient) mainUI() { widgetBase: widgetBase{width: 100, name: "newcontact"}, text: "Add", }, + Button{ + widgetBase: widgetBase{width: 100, name: "introduce"}, + text: "Introduce", + }, }, }, }, @@ -663,6 +668,8 @@ func (c *guiClient) mainUI() { switch click.name { case "newcontact": nextEvent = c.newContactUI(nil) + case "introduce": + nextEvent = c.introduceUI(0) case "compose": nextEvent = c.composeUI(nil, nil) } @@ -2180,6 +2187,16 @@ func (c *guiClient) showContact(id uint64) interface{} { text: "Delete", }}, }, + {{1, 1, nil}}, + { + {1, 1, Button{ + widgetBase: widgetBase{ + name: "introduceTo", + insensitive: contact.isPending || contact.revokedUs, + }, + text: "Introduce", + }}, + }, }, } @@ -2280,12 +2297,224 @@ func (c *guiClient) showContact(id uint64) interface{} { c.gui.Signal() c.save() + case "introduceTo": + return c.introduceUI(contact.id) } } panic("unreachable") } +func (c *guiClient) introduceUI(id uint64) interface{} { + const ( + contactCheckBoxPrefix = "contactchecked-" + ) + + var contactIds []uint64 + var contactLabels []string + var contactChecks []bool + var contactsBoxes [][]GridE + var messageBody string + var messageCompose = []GridE{ + {1, 1, nil}, + {10, 1, Scrolled{ + widgetBase: widgetBase{expand: true, fill: true}, + horizontal: true, + child: TextView{ + widgetBase: widgetBase{expand: true, fill: true, name: "body"}, + editable: true, + wrap: true, + updateOnChange: true, + spellCheck: true, + text: messageBody, + }, + }}, + {1, 1, Button{ + widgetBase: widgetBase{width: 40, name: "doIntroduce"}, + text: "Introduce", + }}, + } + var contactsBoxesLine []GridE + contactsBoxesLine = []GridE{{1, 1, nil}} + var i int + i = 0 + for _, contact := range c.contacts { + if contact.isPending || contact.revokedUs { + continue + } + contactLabels = append(contactLabels, contact.name) + contactIds = append(contactIds, contact.id) + contactChecks = append(contactChecks, false) + contactsBoxesLine = append(contactsBoxesLine, + GridE{3, 1, CheckButton{ + widgetBase: widgetBase{ + name: fmt.Sprintf("%s%d", contactCheckBoxPrefix, i), + }, + checked: false, + text: contact.name, + }}, + GridE{1, 1, nil}) + if i%3 == 2 { + contactsBoxes = append(contactsBoxes, contactsBoxesLine) + contactsBoxesLine = []GridE{{1, 1, nil}} + } + i++ + } + contactsBoxes = append(contactsBoxes, contactsBoxesLine) + + var preSelected string + if id != 0 { + if c.contacts[id].isPending || c.contacts[id].revokedUs { + id = 0 + } else { + preSelected = c.contacts[id].name + } + } + if id == 0 { + preSelected = contactLabels[0] + } + + grid1 := Grid{ + widgetBase: widgetBase{name: "grid", margin: 5}, + rowSpacing: 8, + colSpacing: 3, + rows: append([][]GridE{ + { + {6, 1, Label{text: "Introduce a group of contacts to one another, revealing them to one another and exposing that you know every member of the group.", wrap: 200}}, + {1, 1, nil}, + {6, 1, Label{text: "Introduce one contact to group of contacts without revealing them to one another, and exposing that you the group to one contact.", wrap: 200}}, + }, { + {1, 1, nil}, + {1, 1, nil}, + {1, 1, Button{ + widgetBase: widgetBase{width: 40, name: "introduceAllWay"}, + text: "All-Way", + }}, + {1, 1, nil}, + {1, 1, nil}, + {1, 1, nil}, + {1, 1, nil}, + {1, 1, nil}, + {1, 1, Button{ + widgetBase: widgetBase{width: 40, name: "introduceOneMany"}, + text: "One-to-Many", + }}, + {3, 1, Combo{ + widgetBase: widgetBase{name: "selectedOneMany"}, + labels: contactLabels, + preSelected: preSelected, + }, + }, + {1, 1, nil}, + }, + {{1, 1, nil}}, + { + {9, 1, Label{text: "Select a group of contacts :", wrap: 400}}, + }, + }, contactsBoxes...), + } + grid1.rows = append(grid1.rows, messageCompose) + + getId := func(name string) uint64 { + for i, n := range contactLabels { + if n == name { + return contactIds[i] + } + } + return 0 + } + sensitivity := func() { + c.gui.Actions() <- Sensitive{name: "introduceAllWay", sensitive: id != 0} + c.gui.Actions() <- Sensitive{name: "introduceOneMany", sensitive: id == 0} + c.gui.Actions() <- Sensitive{name: "selectedOneMany", sensitive: id != 0} + for i, id0 := range contactIds { + c.gui.Actions() <- Sensitive{name: fmt.Sprintf("%s%d", contactCheckBoxPrefix, i), sensitive: id0 != id} + } + c.gui.Signal() + } + + //nextRow := len(grid.rows) + + c.gui.Actions() <- SetChild{name: "right", child: rightPane("INTRODUCE CONTACTS", grid1, nil, nil)} + c.gui.Actions() <- UIState{uiStateIntroduceContact} + c.gui.Signal() + + sensitivity() + + for { + event, wanted := c.nextEvent(0) + if wanted { + return event + } + + // if update, ok := event.(Update); ok { + // overSize = c.updateUsage(validContactSelected, draft) + // draft.body = update.text + // c.gui.Signal() + // continue + // } + + click, ok := event.(Click) + if !ok { + continue + } + switch { + case click.name == "introduceAllWay": + id = 0 + sensitivity() + continue + case click.name == "introduceOneMany": + id = getId(click.combos["selectedOneMany"]) + sensitivity() + continue + case click.name == "selectedOneMany": + id = getId(click.combos["selectedOneMany"]) + sensitivity() + continue + case strings.HasPrefix(click.name, contactCheckBoxPrefix): + i, ok := strconv.Atoi(click.name[len(contactCheckBoxPrefix):]) + if ok != nil || i >= len(contactIds) { + continue + } + contactChecks[i] = click.checks[click.name] + continue + case click.name == "doIntroduce": + var cl contactList + if id != 0 { + cl = append(cl, c.contacts[id]) + } + for i = 0; i < len(contactIds); i++ { + if contactIds[i] == id { + continue + } + if contactChecks[i] { + cl = append(cl, c.contacts[contactIds[i]]) + } + } + + var urls []string + if id != 0 { + urls = c.introducePandaMessages_onemany(cl) + } else { + urls = c.introducePandaMessages_group(cl) + } + for i := range cl { + draft := c.newDraft(cl[i], nil) + draft.body = messageBody + introducePandaMessageDesc + urls[i] + c.sendDraft(draft) + c.log.Printf("Sending introduction message to %s\n", cl[i].name) + } + c.save() + + c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} + c.gui.Actions() <- UIState{uiStateMain} + c.gui.Signal() + return nil + } + + } +} + func (c *guiClient) newContactUI(contact *Contact) interface{} { var name string existing := contact != nil From 1c9936ebb8029d56191454a52f3d771f9987d11f Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 7 Nov 2014 22:20:31 +0100 Subject: [PATCH 12/36] Save social graph information in the state file Aka the Dark Web of Trust data --- client/client.go | 4 ++++ client/disk.go | 14 ++++++++++++++ client/disk/client.proto | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/client/client.go b/client/client.go index 7b82c6e..1cd6463 100644 --- a/client/client.go +++ b/client/client.go @@ -496,6 +496,10 @@ type Contact struct { // New ratchet support. ratchet *ratchet.Ratchet + introducedBy uint64 + verifiedBy []uint64 + introducedTo []uint64 + cliId cliId } diff --git a/client/disk.go b/client/disk.go index 7252270..efd7549 100644 --- a/client/disk.go +++ b/client/disk.go @@ -106,6 +106,16 @@ func (c *client) unmarshal(state *disk.State) error { } } + if cont.IntroducedBy != nil { + contact.introducedBy = *cont.IntroducedBy + } + if cont.VerifiedBy != nil && len(cont.VerifiedBy) > 0 { + contact.verifiedBy = cont.VerifiedBy + } + if cont.IntroducedTo != nil && len(cont.IntroducedTo) > 0 { + contact.introducedTo = cont.IntroducedTo + } + if cont.IsPending != nil && *cont.IsPending { contact.isPending = true } @@ -270,7 +280,11 @@ func (c *client) marshal() []byte { PandaKeyExchange: contact.pandaKeyExchange, PandaError: proto.String(contact.pandaResult), RevokedUs: proto.Bool(contact.revokedUs), + IntroducedBy: proto.Uint64(contact.introducedBy), + IntroducedTo: contact.introducedTo, + VerifiedBy: contact.verifiedBy, } + cont.TheirIdentityPublic = contact.theirIdentityPublic[:] if !contact.isPending { cont.MyGroupKey = contact.myGroupKey.Marshal() diff --git a/client/disk/client.proto b/client/disk/client.proto index ff47104..7ec7d15 100644 --- a/client/disk/client.proto +++ b/client/disk/client.proto @@ -71,6 +71,10 @@ message Contact { repeated Event events = 22; optional bool is_pending = 15 [ default = false ]; + + optional fixed64 introduced_by = 23; + repeated fixed64 verified_by = 24; + repeated fixed64 introduced_to = 25; } message RatchetState { @@ -137,6 +141,14 @@ message Draft { repeated protos.Message.Detachment detachments = 7; } +// We propose storing old, alternative, and proposed user names +// message AltUserName { +// required fixed64 id = 1; +// required string name = 2; +// required int64 time = 3; +// required int reason = 5 +// } + message State { required bytes identity = 1; required bytes public = 2; From 4427d65346802b6a940d6d416389a20f2d2e0bd7 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Thu, 13 Nov 2014 18:25:41 +0100 Subject: [PATCH 13/36] Use ./goprotoc.sh to recompile client/disk/client.proto --- client/disk/client.pb.go | 735 +++++++++++++++++++++------------------ 1 file changed, 392 insertions(+), 343 deletions(-) diff --git a/client/disk/client.pb.go b/client/disk/client.pb.go index 1f29160..8738592 100644 --- a/client/disk/client.pb.go +++ b/client/disk/client.pb.go @@ -1,65 +1,89 @@ // Code generated by protoc-gen-go. -// source: github.com/agl/pond/client/disk/client.proto +// source: disk/client.proto // DO NOT EDIT! +/* +Package disk is a generated protocol buffer package. + +It is generated from these files: + disk/client.proto + +It has these top-level messages: + Header + Contact + RatchetState + Inbox + Outbox + Draft + State +*/ package disk import proto "github.com/golang/protobuf/proto" -import json "encoding/json" import math "math" import protos "github.com/agl/pond/protos" -// Reference proto, json, and math imports to suppress error if they are not otherwise used. +// Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal -var _ = &json.SyntaxError{} var _ = math.Inf +// Header is placed at the beginning of the state file and is *unencrypted and +// unauthenticated*. Its purpose is only to describe how to decrypt the +// remainder of the file. type Header struct { - NonceSmearCopies *int32 `protobuf:"varint,1,opt,name=nonce_smear_copies,def=1365" json:"nonce_smear_copies,omitempty"` - KdfSalt []byte `protobuf:"bytes,2,opt,name=kdf_salt" json:"kdf_salt,omitempty"` - Scrypt *Header_SCrypt `protobuf:"bytes,3,opt,name=scrypt" json:"scrypt,omitempty"` - TpmNvram *Header_TPM `protobuf:"bytes,4,opt,name=tpm_nvram" json:"tpm_nvram,omitempty"` - NoErasureStorage *bool `protobuf:"varint,5,opt,name=no_erasure_storage" json:"no_erasure_storage,omitempty"` - XXX_unrecognized []byte `json:"-"` + // nonce_smear_copies contains the number of copies of the nonce that + // follow the header. Each copy of the nonce is different and, XORed + // together, they result in the real nonce. The intent is that this may + // make recovery of old state files more difficult on HDDs. + NonceSmearCopies *int32 `protobuf:"varint,1,opt,name=nonce_smear_copies,def=1365" json:"nonce_smear_copies,omitempty"` + // kdf_salt contains the salt for the KDF function. + KdfSalt []byte `protobuf:"bytes,2,opt,name=kdf_salt" json:"kdf_salt,omitempty"` + Scrypt *Header_SCrypt `protobuf:"bytes,3,opt,name=scrypt" json:"scrypt,omitempty"` + TpmNvram *Header_TPM `protobuf:"bytes,4,opt,name=tpm_nvram" json:"tpm_nvram,omitempty"` + // no_erasure_storage exists to signal that there is no erasure storage + // for this state file, as opposed to the state file using a method + // that isn't recognised by the client. + NoErasureStorage *bool `protobuf:"varint,5,opt,name=no_erasure_storage" json:"no_erasure_storage,omitempty"` + XXX_unrecognized []byte `json:"-"` } -func (this *Header) Reset() { *this = Header{} } -func (this *Header) String() string { return proto.CompactTextString(this) } -func (*Header) ProtoMessage() {} +func (m *Header) Reset() { *m = Header{} } +func (m *Header) String() string { return proto.CompactTextString(m) } +func (*Header) ProtoMessage() {} const Default_Header_NonceSmearCopies int32 = 1365 -func (this *Header) GetNonceSmearCopies() int32 { - if this != nil && this.NonceSmearCopies != nil { - return *this.NonceSmearCopies +func (m *Header) GetNonceSmearCopies() int32 { + if m != nil && m.NonceSmearCopies != nil { + return *m.NonceSmearCopies } return Default_Header_NonceSmearCopies } -func (this *Header) GetKdfSalt() []byte { - if this != nil { - return this.KdfSalt +func (m *Header) GetKdfSalt() []byte { + if m != nil { + return m.KdfSalt } return nil } -func (this *Header) GetScrypt() *Header_SCrypt { - if this != nil { - return this.Scrypt +func (m *Header) GetScrypt() *Header_SCrypt { + if m != nil { + return m.Scrypt } return nil } -func (this *Header) GetTpmNvram() *Header_TPM { - if this != nil { - return this.TpmNvram +func (m *Header) GetTpmNvram() *Header_TPM { + if m != nil { + return m.TpmNvram } return nil } -func (this *Header) GetNoErasureStorage() bool { - if this != nil && this.NoErasureStorage != nil { - return *this.NoErasureStorage +func (m *Header) GetNoErasureStorage() bool { + if m != nil && m.NoErasureStorage != nil { + return *m.NoErasureStorage } return false } @@ -71,47 +95,48 @@ type Header_SCrypt struct { XXX_unrecognized []byte `json:"-"` } -func (this *Header_SCrypt) Reset() { *this = Header_SCrypt{} } -func (this *Header_SCrypt) String() string { return proto.CompactTextString(this) } -func (*Header_SCrypt) ProtoMessage() {} +func (m *Header_SCrypt) Reset() { *m = Header_SCrypt{} } +func (m *Header_SCrypt) String() string { return proto.CompactTextString(m) } +func (*Header_SCrypt) ProtoMessage() {} const Default_Header_SCrypt_N int32 = 32768 const Default_Header_SCrypt_R int32 = 16 const Default_Header_SCrypt_P int32 = 1 -func (this *Header_SCrypt) GetN() int32 { - if this != nil && this.N != nil { - return *this.N +func (m *Header_SCrypt) GetN() int32 { + if m != nil && m.N != nil { + return *m.N } return Default_Header_SCrypt_N } -func (this *Header_SCrypt) GetR() int32 { - if this != nil && this.R != nil { - return *this.R +func (m *Header_SCrypt) GetR() int32 { + if m != nil && m.R != nil { + return *m.R } return Default_Header_SCrypt_R } -func (this *Header_SCrypt) GetP() int32 { - if this != nil && this.P != nil { - return *this.P +func (m *Header_SCrypt) GetP() int32 { + if m != nil && m.P != nil { + return *m.P } return Default_Header_SCrypt_P } +// TPM contains information about an erasure key stored in TPM NVRAM. type Header_TPM struct { Index *uint32 `protobuf:"varint,1,req,name=index" json:"index,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *Header_TPM) Reset() { *this = Header_TPM{} } -func (this *Header_TPM) String() string { return proto.CompactTextString(this) } -func (*Header_TPM) ProtoMessage() {} +func (m *Header_TPM) Reset() { *m = Header_TPM{} } +func (m *Header_TPM) String() string { return proto.CompactTextString(m) } +func (*Header_TPM) ProtoMessage() {} -func (this *Header_TPM) GetIndex() uint32 { - if this != nil && this.Index != nil { - return *this.Index +func (m *Header_TPM) GetIndex() uint32 { + if m != nil && m.Index != nil { + return *m.Index } return 0 } @@ -139,189 +164,213 @@ type Contact struct { PreviousTags []*Contact_PreviousTag `protobuf:"bytes,17,rep,name=previous_tags" json:"previous_tags,omitempty"` Events []*Contact_Event `protobuf:"bytes,22,rep,name=events" json:"events,omitempty"` IsPending *bool `protobuf:"varint,15,opt,name=is_pending,def=0" json:"is_pending,omitempty"` + IntroducedBy *uint64 `protobuf:"fixed64,23,opt,name=introduced_by" json:"introduced_by,omitempty"` + VerifiedBy []uint64 `protobuf:"fixed64,24,rep,name=verified_by" json:"verified_by,omitempty"` + IntroducedTo []uint64 `protobuf:"fixed64,25,rep,name=introduced_to" json:"introduced_to,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *Contact) Reset() { *this = Contact{} } -func (this *Contact) String() string { return proto.CompactTextString(this) } -func (*Contact) ProtoMessage() {} +func (m *Contact) Reset() { *m = Contact{} } +func (m *Contact) String() string { return proto.CompactTextString(m) } +func (*Contact) ProtoMessage() {} const Default_Contact_IsPending bool = false -func (this *Contact) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id +func (m *Contact) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Contact) GetName() string { - if this != nil && this.Name != nil { - return *this.Name +func (m *Contact) GetName() string { + if m != nil && m.Name != nil { + return *m.Name } return "" } -func (this *Contact) GetGroupKey() []byte { - if this != nil { - return this.GroupKey +func (m *Contact) GetGroupKey() []byte { + if m != nil { + return m.GroupKey } return nil } -func (this *Contact) GetSupportedVersion() int32 { - if this != nil && this.SupportedVersion != nil { - return *this.SupportedVersion +func (m *Contact) GetSupportedVersion() int32 { + if m != nil && m.SupportedVersion != nil { + return *m.SupportedVersion } return 0 } -func (this *Contact) GetKeyExchangeBytes() []byte { - if this != nil { - return this.KeyExchangeBytes +func (m *Contact) GetKeyExchangeBytes() []byte { + if m != nil { + return m.KeyExchangeBytes } return nil } -func (this *Contact) GetPandaKeyExchange() []byte { - if this != nil { - return this.PandaKeyExchange +func (m *Contact) GetPandaKeyExchange() []byte { + if m != nil { + return m.PandaKeyExchange } return nil } -func (this *Contact) GetPandaError() string { - if this != nil && this.PandaError != nil { - return *this.PandaError +func (m *Contact) GetPandaError() string { + if m != nil && m.PandaError != nil { + return *m.PandaError } return "" } -func (this *Contact) GetTheirGroup() []byte { - if this != nil { - return this.TheirGroup +func (m *Contact) GetTheirGroup() []byte { + if m != nil { + return m.TheirGroup } return nil } -func (this *Contact) GetMyGroupKey() []byte { - if this != nil { - return this.MyGroupKey +func (m *Contact) GetMyGroupKey() []byte { + if m != nil { + return m.MyGroupKey } return nil } -func (this *Contact) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *Contact) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } -func (this *Contact) GetTheirServer() string { - if this != nil && this.TheirServer != nil { - return *this.TheirServer +func (m *Contact) GetTheirServer() string { + if m != nil && m.TheirServer != nil { + return *m.TheirServer } return "" } -func (this *Contact) GetTheirPub() []byte { - if this != nil { - return this.TheirPub +func (m *Contact) GetTheirPub() []byte { + if m != nil { + return m.TheirPub } return nil } -func (this *Contact) GetTheirIdentityPublic() []byte { - if this != nil { - return this.TheirIdentityPublic +func (m *Contact) GetTheirIdentityPublic() []byte { + if m != nil { + return m.TheirIdentityPublic } return nil } -func (this *Contact) GetRevokedUs() bool { - if this != nil && this.RevokedUs != nil { - return *this.RevokedUs +func (m *Contact) GetRevokedUs() bool { + if m != nil && m.RevokedUs != nil { + return *m.RevokedUs } return false } -func (this *Contact) GetLastPrivate() []byte { - if this != nil { - return this.LastPrivate +func (m *Contact) GetLastPrivate() []byte { + if m != nil { + return m.LastPrivate } return nil } -func (this *Contact) GetCurrentPrivate() []byte { - if this != nil { - return this.CurrentPrivate +func (m *Contact) GetCurrentPrivate() []byte { + if m != nil { + return m.CurrentPrivate } return nil } -func (this *Contact) GetTheirLastPublic() []byte { - if this != nil { - return this.TheirLastPublic +func (m *Contact) GetTheirLastPublic() []byte { + if m != nil { + return m.TheirLastPublic } return nil } -func (this *Contact) GetTheirCurrentPublic() []byte { - if this != nil { - return this.TheirCurrentPublic +func (m *Contact) GetTheirCurrentPublic() []byte { + if m != nil { + return m.TheirCurrentPublic } return nil } -func (this *Contact) GetRatchet() *RatchetState { - if this != nil { - return this.Ratchet +func (m *Contact) GetRatchet() *RatchetState { + if m != nil { + return m.Ratchet } return nil } -func (this *Contact) GetPreviousTags() []*Contact_PreviousTag { - if this != nil { - return this.PreviousTags +func (m *Contact) GetPreviousTags() []*Contact_PreviousTag { + if m != nil { + return m.PreviousTags } return nil } -func (this *Contact) GetEvents() []*Contact_Event { - if this != nil { - return this.Events +func (m *Contact) GetEvents() []*Contact_Event { + if m != nil { + return m.Events } return nil } -func (this *Contact) GetIsPending() bool { - if this != nil && this.IsPending != nil { - return *this.IsPending +func (m *Contact) GetIsPending() bool { + if m != nil && m.IsPending != nil { + return *m.IsPending } return Default_Contact_IsPending } +func (m *Contact) GetIntroducedBy() uint64 { + if m != nil && m.IntroducedBy != nil { + return *m.IntroducedBy + } + return 0 +} + +func (m *Contact) GetVerifiedBy() []uint64 { + if m != nil { + return m.VerifiedBy + } + return nil +} + +func (m *Contact) GetIntroducedTo() []uint64 { + if m != nil { + return m.IntroducedTo + } + return nil +} + type Contact_PreviousTag struct { Tag []byte `protobuf:"bytes,1,req,name=tag" json:"tag,omitempty"` Expired *int64 `protobuf:"varint,2,req,name=expired" json:"expired,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *Contact_PreviousTag) Reset() { *this = Contact_PreviousTag{} } -func (this *Contact_PreviousTag) String() string { return proto.CompactTextString(this) } -func (*Contact_PreviousTag) ProtoMessage() {} +func (m *Contact_PreviousTag) Reset() { *m = Contact_PreviousTag{} } +func (m *Contact_PreviousTag) String() string { return proto.CompactTextString(m) } +func (*Contact_PreviousTag) ProtoMessage() {} -func (this *Contact_PreviousTag) GetTag() []byte { - if this != nil { - return this.Tag +func (m *Contact_PreviousTag) GetTag() []byte { + if m != nil { + return m.Tag } return nil } -func (this *Contact_PreviousTag) GetExpired() int64 { - if this != nil && this.Expired != nil { - return *this.Expired +func (m *Contact_PreviousTag) GetExpired() int64 { + if m != nil && m.Expired != nil { + return *m.Expired } return 0 } @@ -332,20 +381,20 @@ type Contact_Event struct { XXX_unrecognized []byte `json:"-"` } -func (this *Contact_Event) Reset() { *this = Contact_Event{} } -func (this *Contact_Event) String() string { return proto.CompactTextString(this) } -func (*Contact_Event) ProtoMessage() {} +func (m *Contact_Event) Reset() { *m = Contact_Event{} } +func (m *Contact_Event) String() string { return proto.CompactTextString(m) } +func (*Contact_Event) ProtoMessage() {} -func (this *Contact_Event) GetTime() int64 { - if this != nil && this.Time != nil { - return *this.Time +func (m *Contact_Event) GetTime() int64 { + if m != nil && m.Time != nil { + return *m.Time } return 0 } -func (this *Contact_Event) GetMessage() string { - if this != nil && this.Message != nil { - return *this.Message +func (m *Contact_Event) GetMessage() string { + if m != nil && m.Message != nil { + return *m.Message } return "" } @@ -371,125 +420,125 @@ type RatchetState struct { XXX_unrecognized []byte `json:"-"` } -func (this *RatchetState) Reset() { *this = RatchetState{} } -func (this *RatchetState) String() string { return proto.CompactTextString(this) } -func (*RatchetState) ProtoMessage() {} +func (m *RatchetState) Reset() { *m = RatchetState{} } +func (m *RatchetState) String() string { return proto.CompactTextString(m) } +func (*RatchetState) ProtoMessage() {} -func (this *RatchetState) GetRootKey() []byte { - if this != nil { - return this.RootKey +func (m *RatchetState) GetRootKey() []byte { + if m != nil { + return m.RootKey } return nil } -func (this *RatchetState) GetSendHeaderKey() []byte { - if this != nil { - return this.SendHeaderKey +func (m *RatchetState) GetSendHeaderKey() []byte { + if m != nil { + return m.SendHeaderKey } return nil } -func (this *RatchetState) GetRecvHeaderKey() []byte { - if this != nil { - return this.RecvHeaderKey +func (m *RatchetState) GetRecvHeaderKey() []byte { + if m != nil { + return m.RecvHeaderKey } return nil } -func (this *RatchetState) GetNextSendHeaderKey() []byte { - if this != nil { - return this.NextSendHeaderKey +func (m *RatchetState) GetNextSendHeaderKey() []byte { + if m != nil { + return m.NextSendHeaderKey } return nil } -func (this *RatchetState) GetNextRecvHeaderKey() []byte { - if this != nil { - return this.NextRecvHeaderKey +func (m *RatchetState) GetNextRecvHeaderKey() []byte { + if m != nil { + return m.NextRecvHeaderKey } return nil } -func (this *RatchetState) GetSendChainKey() []byte { - if this != nil { - return this.SendChainKey +func (m *RatchetState) GetSendChainKey() []byte { + if m != nil { + return m.SendChainKey } return nil } -func (this *RatchetState) GetRecvChainKey() []byte { - if this != nil { - return this.RecvChainKey +func (m *RatchetState) GetRecvChainKey() []byte { + if m != nil { + return m.RecvChainKey } return nil } -func (this *RatchetState) GetSendRatchetPrivate() []byte { - if this != nil { - return this.SendRatchetPrivate +func (m *RatchetState) GetSendRatchetPrivate() []byte { + if m != nil { + return m.SendRatchetPrivate } return nil } -func (this *RatchetState) GetRecvRatchetPublic() []byte { - if this != nil { - return this.RecvRatchetPublic +func (m *RatchetState) GetRecvRatchetPublic() []byte { + if m != nil { + return m.RecvRatchetPublic } return nil } -func (this *RatchetState) GetSendCount() uint32 { - if this != nil && this.SendCount != nil { - return *this.SendCount +func (m *RatchetState) GetSendCount() uint32 { + if m != nil && m.SendCount != nil { + return *m.SendCount } return 0 } -func (this *RatchetState) GetRecvCount() uint32 { - if this != nil && this.RecvCount != nil { - return *this.RecvCount +func (m *RatchetState) GetRecvCount() uint32 { + if m != nil && m.RecvCount != nil { + return *m.RecvCount } return 0 } -func (this *RatchetState) GetPrevSendCount() uint32 { - if this != nil && this.PrevSendCount != nil { - return *this.PrevSendCount +func (m *RatchetState) GetPrevSendCount() uint32 { + if m != nil && m.PrevSendCount != nil { + return *m.PrevSendCount } return 0 } -func (this *RatchetState) GetRatchet() bool { - if this != nil && this.Ratchet != nil { - return *this.Ratchet +func (m *RatchetState) GetRatchet() bool { + if m != nil && m.Ratchet != nil { + return *m.Ratchet } return false } -func (this *RatchetState) GetV2() bool { - if this != nil && this.V2 != nil { - return *this.V2 +func (m *RatchetState) GetV2() bool { + if m != nil && m.V2 != nil { + return *m.V2 } return false } -func (this *RatchetState) GetPrivate0() []byte { - if this != nil { - return this.Private0 +func (m *RatchetState) GetPrivate0() []byte { + if m != nil { + return m.Private0 } return nil } -func (this *RatchetState) GetPrivate1() []byte { - if this != nil { - return this.Private1 +func (m *RatchetState) GetPrivate1() []byte { + if m != nil { + return m.Private1 } return nil } -func (this *RatchetState) GetSavedKeys() []*RatchetState_SavedKeys { - if this != nil { - return this.SavedKeys +func (m *RatchetState) GetSavedKeys() []*RatchetState_SavedKeys { + if m != nil { + return m.SavedKeys } return nil } @@ -500,20 +549,20 @@ type RatchetState_SavedKeys struct { XXX_unrecognized []byte `json:"-"` } -func (this *RatchetState_SavedKeys) Reset() { *this = RatchetState_SavedKeys{} } -func (this *RatchetState_SavedKeys) String() string { return proto.CompactTextString(this) } -func (*RatchetState_SavedKeys) ProtoMessage() {} +func (m *RatchetState_SavedKeys) Reset() { *m = RatchetState_SavedKeys{} } +func (m *RatchetState_SavedKeys) String() string { return proto.CompactTextString(m) } +func (*RatchetState_SavedKeys) ProtoMessage() {} -func (this *RatchetState_SavedKeys) GetHeaderKey() []byte { - if this != nil { - return this.HeaderKey +func (m *RatchetState_SavedKeys) GetHeaderKey() []byte { + if m != nil { + return m.HeaderKey } return nil } -func (this *RatchetState_SavedKeys) GetMessageKeys() []*RatchetState_SavedKeys_MessageKey { - if this != nil { - return this.MessageKeys +func (m *RatchetState_SavedKeys) GetMessageKeys() []*RatchetState_SavedKeys_MessageKey { + if m != nil { + return m.MessageKeys } return nil } @@ -525,27 +574,27 @@ type RatchetState_SavedKeys_MessageKey struct { XXX_unrecognized []byte `json:"-"` } -func (this *RatchetState_SavedKeys_MessageKey) Reset() { *this = RatchetState_SavedKeys_MessageKey{} } -func (this *RatchetState_SavedKeys_MessageKey) String() string { return proto.CompactTextString(this) } -func (*RatchetState_SavedKeys_MessageKey) ProtoMessage() {} +func (m *RatchetState_SavedKeys_MessageKey) Reset() { *m = RatchetState_SavedKeys_MessageKey{} } +func (m *RatchetState_SavedKeys_MessageKey) String() string { return proto.CompactTextString(m) } +func (*RatchetState_SavedKeys_MessageKey) ProtoMessage() {} -func (this *RatchetState_SavedKeys_MessageKey) GetNum() uint32 { - if this != nil && this.Num != nil { - return *this.Num +func (m *RatchetState_SavedKeys_MessageKey) GetNum() uint32 { + if m != nil && m.Num != nil { + return *m.Num } return 0 } -func (this *RatchetState_SavedKeys_MessageKey) GetKey() []byte { - if this != nil { - return this.Key +func (m *RatchetState_SavedKeys_MessageKey) GetKey() []byte { + if m != nil { + return m.Key } return nil } -func (this *RatchetState_SavedKeys_MessageKey) GetCreationTime() int64 { - if this != nil && this.CreationTime != nil { - return *this.CreationTime +func (m *RatchetState_SavedKeys_MessageKey) GetCreationTime() int64 { + if m != nil && m.CreationTime != nil { + return *m.CreationTime } return 0 } @@ -562,64 +611,64 @@ type Inbox struct { XXX_unrecognized []byte `json:"-"` } -func (this *Inbox) Reset() { *this = Inbox{} } -func (this *Inbox) String() string { return proto.CompactTextString(this) } -func (*Inbox) ProtoMessage() {} +func (m *Inbox) Reset() { *m = Inbox{} } +func (m *Inbox) String() string { return proto.CompactTextString(m) } +func (*Inbox) ProtoMessage() {} const Default_Inbox_Retained bool = false -func (this *Inbox) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id +func (m *Inbox) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Inbox) GetFrom() uint64 { - if this != nil && this.From != nil { - return *this.From +func (m *Inbox) GetFrom() uint64 { + if m != nil && m.From != nil { + return *m.From } return 0 } -func (this *Inbox) GetReceivedTime() int64 { - if this != nil && this.ReceivedTime != nil { - return *this.ReceivedTime +func (m *Inbox) GetReceivedTime() int64 { + if m != nil && m.ReceivedTime != nil { + return *m.ReceivedTime } return 0 } -func (this *Inbox) GetAcked() bool { - if this != nil && this.Acked != nil { - return *this.Acked +func (m *Inbox) GetAcked() bool { + if m != nil && m.Acked != nil { + return *m.Acked } return false } -func (this *Inbox) GetMessage() []byte { - if this != nil { - return this.Message +func (m *Inbox) GetMessage() []byte { + if m != nil { + return m.Message } return nil } -func (this *Inbox) GetRead() bool { - if this != nil && this.Read != nil { - return *this.Read +func (m *Inbox) GetRead() bool { + if m != nil && m.Read != nil { + return *m.Read } return false } -func (this *Inbox) GetSealed() []byte { - if this != nil { - return this.Sealed +func (m *Inbox) GetSealed() []byte { + if m != nil { + return m.Sealed } return nil } -func (this *Inbox) GetRetained() bool { - if this != nil && this.Retained != nil { - return *this.Retained +func (m *Inbox) GetRetained() bool { + if m != nil && m.Retained != nil { + return *m.Retained } return Default_Inbox_Retained } @@ -637,69 +686,69 @@ type Outbox struct { XXX_unrecognized []byte `json:"-"` } -func (this *Outbox) Reset() { *this = Outbox{} } -func (this *Outbox) String() string { return proto.CompactTextString(this) } -func (*Outbox) ProtoMessage() {} +func (m *Outbox) Reset() { *m = Outbox{} } +func (m *Outbox) String() string { return proto.CompactTextString(m) } +func (*Outbox) ProtoMessage() {} -func (this *Outbox) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id +func (m *Outbox) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Outbox) GetTo() uint64 { - if this != nil && this.To != nil { - return *this.To +func (m *Outbox) GetTo() uint64 { + if m != nil && m.To != nil { + return *m.To } return 0 } -func (this *Outbox) GetServer() string { - if this != nil && this.Server != nil { - return *this.Server +func (m *Outbox) GetServer() string { + if m != nil && m.Server != nil { + return *m.Server } return "" } -func (this *Outbox) GetCreated() int64 { - if this != nil && this.Created != nil { - return *this.Created +func (m *Outbox) GetCreated() int64 { + if m != nil && m.Created != nil { + return *m.Created } return 0 } -func (this *Outbox) GetSent() int64 { - if this != nil && this.Sent != nil { - return *this.Sent +func (m *Outbox) GetSent() int64 { + if m != nil && m.Sent != nil { + return *m.Sent } return 0 } -func (this *Outbox) GetMessage() []byte { - if this != nil { - return this.Message +func (m *Outbox) GetMessage() []byte { + if m != nil { + return m.Message } return nil } -func (this *Outbox) GetRequest() []byte { - if this != nil { - return this.Request +func (m *Outbox) GetRequest() []byte { + if m != nil { + return m.Request } return nil } -func (this *Outbox) GetAcked() int64 { - if this != nil && this.Acked != nil { - return *this.Acked +func (m *Outbox) GetAcked() int64 { + if m != nil && m.Acked != nil { + return *m.Acked } return 0 } -func (this *Outbox) GetRevocation() bool { - if this != nil && this.Revocation != nil { - return *this.Revocation +func (m *Outbox) GetRevocation() bool { + if m != nil && m.Revocation != nil { + return *m.Revocation } return false } @@ -715,55 +764,55 @@ type Draft struct { XXX_unrecognized []byte `json:"-"` } -func (this *Draft) Reset() { *this = Draft{} } -func (this *Draft) String() string { return proto.CompactTextString(this) } -func (*Draft) ProtoMessage() {} +func (m *Draft) Reset() { *m = Draft{} } +func (m *Draft) String() string { return proto.CompactTextString(m) } +func (*Draft) ProtoMessage() {} -func (this *Draft) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id +func (m *Draft) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Draft) GetCreated() int64 { - if this != nil && this.Created != nil { - return *this.Created +func (m *Draft) GetCreated() int64 { + if m != nil && m.Created != nil { + return *m.Created } return 0 } -func (this *Draft) GetTo() uint64 { - if this != nil && this.To != nil { - return *this.To +func (m *Draft) GetTo() uint64 { + if m != nil && m.To != nil { + return *m.To } return 0 } -func (this *Draft) GetBody() string { - if this != nil && this.Body != nil { - return *this.Body +func (m *Draft) GetBody() string { + if m != nil && m.Body != nil { + return *m.Body } return "" } -func (this *Draft) GetInReplyTo() uint64 { - if this != nil && this.InReplyTo != nil { - return *this.InReplyTo +func (m *Draft) GetInReplyTo() uint64 { + if m != nil && m.InReplyTo != nil { + return *m.InReplyTo } return 0 } -func (this *Draft) GetAttachments() []*protos.Message_Attachment { - if this != nil { - return this.Attachments +func (m *Draft) GetAttachments() []*protos.Message_Attachment { + if m != nil { + return m.Attachments } return nil } -func (this *Draft) GetDetachments() []*protos.Message_Detachment { - if this != nil { - return this.Detachments +func (m *Draft) GetDetachments() []*protos.Message_Detachment { + if m != nil { + return m.Detachments } return nil } @@ -785,97 +834,97 @@ type State struct { XXX_unrecognized []byte `json:"-"` } -func (this *State) Reset() { *this = State{} } -func (this *State) String() string { return proto.CompactTextString(this) } -func (*State) ProtoMessage() {} +func (m *State) Reset() { *m = State{} } +func (m *State) String() string { return proto.CompactTextString(m) } +func (*State) ProtoMessage() {} -func (this *State) GetIdentity() []byte { - if this != nil { - return this.Identity +func (m *State) GetIdentity() []byte { + if m != nil { + return m.Identity } return nil } -func (this *State) GetPublic() []byte { - if this != nil { - return this.Public +func (m *State) GetPublic() []byte { + if m != nil { + return m.Public } return nil } -func (this *State) GetPrivate() []byte { - if this != nil { - return this.Private +func (m *State) GetPrivate() []byte { + if m != nil { + return m.Private } return nil } -func (this *State) GetServer() string { - if this != nil && this.Server != nil { - return *this.Server +func (m *State) GetServer() string { + if m != nil && m.Server != nil { + return *m.Server } return "" } -func (this *State) GetGroup() []byte { - if this != nil { - return this.Group +func (m *State) GetGroup() []byte { + if m != nil { + return m.Group } return nil } -func (this *State) GetGroupPrivate() []byte { - if this != nil { - return this.GroupPrivate +func (m *State) GetGroupPrivate() []byte { + if m != nil { + return m.GroupPrivate } return nil } -func (this *State) GetPreviousGroupPrivateKeys() []*State_PreviousGroup { - if this != nil { - return this.PreviousGroupPrivateKeys +func (m *State) GetPreviousGroupPrivateKeys() []*State_PreviousGroup { + if m != nil { + return m.PreviousGroupPrivateKeys } return nil } -func (this *State) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *State) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } -func (this *State) GetLastErasureStorageTime() int64 { - if this != nil && this.LastErasureStorageTime != nil { - return *this.LastErasureStorageTime +func (m *State) GetLastErasureStorageTime() int64 { + if m != nil && m.LastErasureStorageTime != nil { + return *m.LastErasureStorageTime } return 0 } -func (this *State) GetContacts() []*Contact { - if this != nil { - return this.Contacts +func (m *State) GetContacts() []*Contact { + if m != nil { + return m.Contacts } return nil } -func (this *State) GetInbox() []*Inbox { - if this != nil { - return this.Inbox +func (m *State) GetInbox() []*Inbox { + if m != nil { + return m.Inbox } return nil } -func (this *State) GetOutbox() []*Outbox { - if this != nil { - return this.Outbox +func (m *State) GetOutbox() []*Outbox { + if m != nil { + return m.Outbox } return nil } -func (this *State) GetDrafts() []*Draft { - if this != nil { - return this.Drafts +func (m *State) GetDrafts() []*Draft { + if m != nil { + return m.Drafts } return nil } @@ -887,27 +936,27 @@ type State_PreviousGroup struct { XXX_unrecognized []byte `json:"-"` } -func (this *State_PreviousGroup) Reset() { *this = State_PreviousGroup{} } -func (this *State_PreviousGroup) String() string { return proto.CompactTextString(this) } -func (*State_PreviousGroup) ProtoMessage() {} +func (m *State_PreviousGroup) Reset() { *m = State_PreviousGroup{} } +func (m *State_PreviousGroup) String() string { return proto.CompactTextString(m) } +func (*State_PreviousGroup) ProtoMessage() {} -func (this *State_PreviousGroup) GetGroup() []byte { - if this != nil { - return this.Group +func (m *State_PreviousGroup) GetGroup() []byte { + if m != nil { + return m.Group } return nil } -func (this *State_PreviousGroup) GetGroupPrivate() []byte { - if this != nil { - return this.GroupPrivate +func (m *State_PreviousGroup) GetGroupPrivate() []byte { + if m != nil { + return m.GroupPrivate } return nil } -func (this *State_PreviousGroup) GetExpired() int64 { - if this != nil && this.Expired != nil { - return *this.Expired +func (m *State_PreviousGroup) GetExpired() int64 { + if m != nil && m.Expired != nil { + return *m.Expired } return 0 } From 2dfce614367448d9881fac804d01273d78df5fb4 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sun, 16 Nov 2014 00:36:28 +0100 Subject: [PATCH 14/36] Fill in a contact's social graph data. --- client/cli.go | 6 +-- client/client.go | 16 ++++++ client/gui.go | 8 +-- client/introduce.go | 120 +++++++++++++++++++++++++++++--------------- 4 files changed, 103 insertions(+), 47 deletions(-) diff --git a/client/cli.go b/client/cli.go index 327306d..c96e424 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1634,7 +1634,7 @@ Handle: } // c.introduceContact_onemany(contact,cl) - urls := c.introducePandaMessages_onemany(cl) + urls := c.introducePandaMessages_onemany(cl, true) for i := range cl { draft := c.newDraft(cl[i], nil) draft.cliId = c.newCliId() @@ -1667,7 +1667,7 @@ Handle: c.Printf("Not OK, what now?") } - urls := c.introducePandaMessages_group(cl) + urls := c.introducePandaMessages_group(cl, true) for i := range cl { draft := c.newDraft(cl[i], nil) draft.cliId = c.newCliId() @@ -1694,7 +1694,7 @@ Handle: return } c.Printf("%s Begining PANDA key exchange with %s\n", termPrefix, pc.name) - c.beginProposedPandaKeyExchange(pc) + c.beginProposedPandaKeyExchange(pc, msg.from) if cmd.Index != "*" { return } diff --git a/client/client.go b/client/client.go index 1cd6463..d276048 100644 --- a/client/client.go +++ b/client/client.go @@ -1112,7 +1112,15 @@ func (c *client) contactByName(name string) (*Contact, bool) { return contact, true } } + return nil, false +} +func (c *client) contactByIdentity(theirIdentityPublic []byte) (*Contact, bool) { + for _, contact := range c.contacts { + if bytes.Equal(contact.theirIdentityPublic[:], theirIdentityPublic) { + return contact, true + } + } return nil, false } @@ -1220,6 +1228,14 @@ func (c *client) deleteContact(contact *Contact) { } } + for id, contact := range c.contacts { + if contact.introducedBy == id { + contact.introducedBy = 0 + } + removeIdSet(&contact.verifiedBy, id) + removeIdSet(&contact.introducedTo, id) + } + c.queueMutex.Lock() var newQueue []*queuedMessage for _, msg := range c.queue { diff --git a/client/gui.go b/client/gui.go index aa4f44c..dedc250 100644 --- a/client/gui.go +++ b/client/gui.go @@ -1607,7 +1607,7 @@ NextEvent: if ok != nil || i >= len(pcs) { panic("invalid greet command") } - contact := c.beginProposedPandaKeyExchange(pcs[i]) + contact := c.beginProposedPandaKeyExchange(pcs[i], msg.from) c.contactsUI.Add(contact.id, contact.name, "pending", indicatorNone) c.contactsUI.Select(contact.id) c.gui.Actions() <- Sensitive{name: click.name, sensitive: false} @@ -2494,15 +2494,15 @@ func (c *guiClient) introduceUI(id uint64) interface{} { var urls []string if id != 0 { - urls = c.introducePandaMessages_onemany(cl) + urls = c.introducePandaMessages_onemany(cl, true) } else { - urls = c.introducePandaMessages_group(cl) + urls = c.introducePandaMessages_group(cl, true) } for i := range cl { draft := c.newDraft(cl[i], nil) draft.body = messageBody + introducePandaMessageDesc + urls[i] c.sendDraft(draft) - c.log.Printf("Sending introduction message to %s\n", cl[i].name) + c.log.Printf("Queued introduction message for %s.", cl[i].name) } c.save() diff --git a/client/introduce.go b/client/introduce.go index df2771b..3ba853b 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -12,65 +12,91 @@ const ( introducePandaMessageDesc = "Introduction URLs for proposed new contacts :\n" ) -func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact) (string, string) { +func addIdSet(set *[]uint64, id uint64) { + for _, s := range *set { + if s == id { + return + } + } + *set = append(*set, id) +} + +func removeIdSet(set *[]uint64, id uint64) { + for i, s := range *set { + if s == id { + *set = append((*set)[:i], (*set)[i+1:]...) + return + } + } +} + +func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (string, string) { panda_secret := panda.NewSecretString(c.rand)[2:] s := func(cnt *Contact) string { return fmt.Sprintf("pond-introduce-panda://%s/%s/%x/\n", url.QueryEscape(cnt.name), panda_secret, cnt.theirIdentityPublic) // no EncodeToString? } - return s(cnt1), s(cnt2) + if real { + addIdSet(&cnt1.introducedTo, cnt2.id) + addIdSet(&cnt2.introducedTo, cnt1.id) + } + return s(cnt2), s(cnt1) +} + +func (c *client) introducePandaMessages(shown, hidden contactList, real bool) ([]string, []string) { + n := len(shown) + len(hidden) + var urls []string = make([]string, n) + cnts := append(shown, hidden...) + for i := 0; i < len(shown); i++ { + for j := i + 1; j < n; j++ { + ui, uj := c.introducePandaMessages_pair(cnts[i], cnts[j], real) + urls[i] += ui + urls[j] += uj + } + } + return urls[0:len(shown)], urls[len(shown):] +} + +func (c *client) introducePandaMessages_onemany(cnts contactList, real bool) []string { + urls1, urls2 := c.introducePandaMessages(contactList{cnts[0]}, cnts[1:], real) + return append(urls1, urls2...) } -func (c *client) introducePandaMessages_onemany(cnts contactList) []string { - var urls []string = make([]string, len(cnts)) +/* +func (c *client) introducePandaMessages_onemany(cnts contactList) ([]string) { + var urls []string = make([]string,len(cnts)) cnt1 := cnts[0] for i, cnt2 := range cnts[1:] { // if i==0 { continue } - u1, u2 := c.introducePandaMessages_pair(cnt1, cnt2) + u1,u2 := c.introducePandaMessages_pair(cnt1,cnt2) urls[0] += u1 urls[i] = u2 } return urls } +*/ -func (c *client) introducePandaMessages_group(cnts contactList) []string { +func (c *client) introducePandaMessages_group(cnts contactList, real bool) []string { + urls, _ := c.introducePandaMessages(cnts, nil, real) + return urls +} + +/* +func (c *client) introducePandaMessages_group(cnts contactList) ([]string) { n := len(cnts) - var urls []string = make([]string, len(cnts)) + var urls []string = make([]string,len(cnts)) // for i := 0; i < n; i++ { urls[i] = "" } for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - ui, uj := c.introducePandaMessages_pair(cnts[i], cnts[j]) + for j := i+1; j < n; j++ { + ui,uj := c.introducePandaMessages_pair(cnts[i],cnts[j]) urls[i] += ui urls[j] += uj } } return urls } - -func (c *client) introducePandaMessages_fancy(shown, hidden contactList) ([]string, []string) { - n := len(shown) + len(hidden) - var urls []string = make([]string, n) - cnts := append(shown, hidden...) - for i := 0; i < len(shown); i++ { - for j := i + 1; j < n; j++ { - ui, uj := c.introducePandaMessages_pair(cnts[i], cnts[j]) - urls[i] += ui - urls[j] += uj - } - } - return urls[0:len(shown)], urls[len(shown):] -} - -// func introducePandaMessages_onemany(cnts contactList) ([]string) { -// urls1,urls2 := introducePandaMessages_fancy({cnts[0]},cnts[1:]) -// return append(urls1,urls2...) -// } - -// func introducePandaMessages_group(cnts contactList) ([]string) { -// urls,_ := introducePandaMessages_fancy(cnts,nil) -// return urls -// } +*/ type ProposedContact struct { sharedSecret string @@ -79,7 +105,7 @@ type ProposedContact struct { id uint64 // zero if new or failed } -func (c *client) checkProposedContactName(sender uint64, pc ProposedContact) { +func (c *client) fixProposedContactName(pc ProposedContact, sender uint64) { // We should consider using JaroWinkler or Levenshtein from // "github.com/antzucaro/matchr" here : // https://godoc.org/github.com/antzucaro/matchr#JaroWinkler @@ -115,6 +141,8 @@ func (c *client) checkProposedContactName(sender uint64, pc ProposedContact) { // Finds and parses all the pond-introduce-panda URLs in a message body. // Returns a list of ProposedContacts from which to create add contact buttons. +// We allow contacts to be added even if they fail most checks here because +// maybe they're the legit contact and the existing one is bad. func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContact { var l []ProposedContact re := regexp.MustCompile("(pond-introduce-panda)://([^/]+)/([^/]+)/([0-9A-Fa-f]{64})/") @@ -130,22 +158,30 @@ func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContac c.log.Printf("Unacceptably weak secret '%s' for %s.", m[urlparse_sharedSecret], m[urlparse_name]) } + var pc ProposedContact pc.sharedSecret = m[urlparse_sharedSecret] + if !hexDecodeSafe(pc.theirIdentityPublic[:], m[urlparse_theirIdentityPublic]) { c.log.Printf("Bad public identity %s, skipping.", m[urlparse_theirIdentityPublic]) continue } + if contact, found := c.contactByIdentity(pc.theirIdentityPublic[:]); found { + pc.id = contact.id + if contact.introducedBy != sender { + addIdSet(&contact.verifiedBy, sender) + } + } + n, err := url.QueryUnescape(m[urlparse_name]) if err != nil { c.log.Printf("Badly escaped name %s, fix using rename.", m[urlparse_name]) } else { pc.name = n } + c.fixProposedContactName(pc, sender) + l = append(l, pc) - // We allow contacts to be added even if they fail these checks because - // maybe they're the legit contact and the existing one is bad. - c.checkProposedContactName(sender, pc) } return l } @@ -161,7 +197,7 @@ func (c *client) parsePandaURLs(msg *InboxMessage) []ProposedContact { // Add a ProposedContact using PANDA once by building panda.SharedSecret and // the basic contact struct to call beginPandaKeyExchange. -func (c *client) beginProposedPandaKeyExchange(pc ProposedContact) *Contact { +func (c *client) beginProposedPandaKeyExchange(pc ProposedContact, introducedBy uint64) *Contact { if len(pc.sharedSecret) == 0 || !panda.IsAcceptableSecretString(pc.sharedSecret) { c.log.Printf("Unacceptably weak secret '%s'.", pc.sharedSecret) return nil @@ -177,9 +213,13 @@ func (c *client) beginProposedPandaKeyExchange(pc ProposedContact) *Contact { isPending: true, id: c.randId(), theirIdentityPublic: pc.theirIdentityPublic, + introducedBy: introducedBy, } - // theirIdentityPublic set distinguishes contacts pending by introduction - // copy(contact.theirIdentityPublic[:], pc.theirIdentityPublic[:]) + // theirIdentityPublic is set only for contacts pending by introduction + // if introducedBy != 0 { + // contact.introducedBy = introducedBy + // copy(contact.theirIdentityPublic[:], pc.theirIdentityPublic[:]) + // } stack := &panda.CardStack{ NumDecks: 1, From 4231404678ed50c623a3d3ec12a53c30ca25e894 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 17 Nov 2014 00:38:28 +0100 Subject: [PATCH 15/36] Show the social graph records in contact information --- client/cli.go | 40 ++++++++++++++++++++++++++++++++++++++++ client/gui.go | 15 +++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/client/cli.go b/client/cli.go index c96e424..5f9d5bd 100644 --- a/client/cli.go +++ b/client/cli.go @@ -2021,6 +2021,24 @@ func (c *cliClient) inputContactList(title string, } } +func (c *client) listContactsAndUnknowns(ids []uint64) string { + unknowns := 0 + listing := "" + for _, id := range ids { + cnt, ok := c.contacts[id] + if ok { + listing += cnt.name + ", " + } else { + unknowns++ + } + } + if unknowns > 0 { + listing += fmt.Sprintf("and %d unknown contacts.", unknowns) + } + listing = strings.TrimSuffix(listing, ", ") + return listing +} + func (c *cliClient) showContact(contact *Contact) { if len(contact.pandaResult) > 0 { c.Printf("%s PANDA error: %s\n", termErrPrefix, terminalEscape(contact.pandaResult, false)) @@ -2046,6 +2064,28 @@ func (c *cliClient) showContact(contact *Contact) { cliRow{cols: []string{"Client version", fmt.Sprintf("%d", contact.supportedVersion)}}, }, } + + if contact.introducedBy != 0 { + cnt, ok := c.contacts[contact.introducedBy] + name := "Unknown" + if ok { + name = terminalEscape(cnt.name, false) + } + table.rows = append(table.rows, + cliRow{cols: []string{"Introduced By", name}}, + ) + } + if len(contact.verifiedBy) > 0 { + table.rows = append(table.rows, + cliRow{cols: []string{"Verified By", terminalEscape(c.listContactsAndUnknowns(contact.verifiedBy), false)}}, + ) + } + if len(contact.introducedTo) > 0 { + table.rows = append(table.rows, + cliRow{cols: []string{"Introduced To", terminalEscape(c.listContactsAndUnknowns(contact.introducedTo), false)}}, + ) + } + table.WriteTo(c.term) if len(contact.events) > 0 { diff --git a/client/gui.go b/client/gui.go index dedc250..c7c76b8 100644 --- a/client/gui.go +++ b/client/gui.go @@ -2161,6 +2161,21 @@ func (c *guiClient) showContact(id uint64) interface{} { entries = append(entries, nvEntry{"KEY EXCHANGE", string(out.Bytes())}) } + if contact.introducedBy != 0 { + cnt, ok := c.contacts[contact.introducedBy] + name := "Unknown" + if ok { + name = cnt.name + } + entries = append(entries, nvEntry{"INTRODUCED BY", name}) + } + if len(contact.verifiedBy) > 0 { + entries = append(entries, nvEntry{"VERIFIED BY", c.listContactsAndUnknowns(contact.verifiedBy)}) + } + if len(contact.introducedTo) > 0 { + entries = append(entries, nvEntry{"INTRODUCED TO", c.listContactsAndUnknowns(contact.introducedTo)}) + } + if len(contact.events) > 0 { eventsText := "" for i, event := range contact.events { From 8e3c24da51a8b0ee6d954e67760f2399688d6cad Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 3 Nov 2014 16:21:43 +0100 Subject: [PATCH 16/36] Refactor composeUI into composeUI and newDraftUI. Simplifies creating messages to specific recipients and makes the code more readable. --- client/gui.go | 90 ++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/client/gui.go b/client/gui.go index c7c76b8..3f5d74d 100644 --- a/client/gui.go +++ b/client/gui.go @@ -658,7 +658,7 @@ func (c *guiClient) mainUI() { } if id, ok := c.draftsUI.Event(event); ok { c.draftsUI.Select(id) - nextEvent = c.composeUI(c.drafts[id], nil) + nextEvent = c.composeUI(c.drafts[id]) } click, ok := event.(Click) @@ -671,7 +671,7 @@ func (c *guiClient) mainUI() { case "introduce": nextEvent = c.introduceUI(0) case "compose": - nextEvent = c.composeUI(nil, nil) + nextEvent = c.composeUI(nil) } } } @@ -1662,7 +1662,7 @@ NextEvent: c.gui.Signal() case click.name == "reply": c.inboxUI.Deselect() - return c.composeUI(nil, msg) + return c.composeUI(c.newDraftUI(msg, 0)) case click.name == "delete": c.inboxUI.Remove(msg.id) c.deleteInboxMsg(msg.id) @@ -1840,7 +1840,7 @@ func (c *guiClient) showOutbox(id uint64) interface{} { c.draftsUI.Select(draft.id) c.drafts[draft.id] = draft c.save() - return c.composeUI(draft, nil) + return c.composeUI(draft) } if click, ok := event.(Click); ok && click.name == "delete" { @@ -3254,9 +3254,39 @@ func (c *guiClient) updateUsage(validContactSelected bool, draft *Draft) bool { return over } -func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} { - if draft != nil && inReplyTo != nil { - panic("draft and inReplyTo both set") +func (c *guiClient) newDraftUI(inReplyTo *InboxMessage, to uint64) (draft *Draft) { + if inReplyTo != nil && to != 0 { + panic("newDraftUI : inReplyTo and to both set") + } + + draft = &Draft{ + id: c.randId(), + created: c.Now(), + } + + if inReplyTo != nil { + draft.inReplyTo = inReplyTo.id + to = inReplyTo.from + draft.body = indentForReply(inReplyTo.message.GetBody()) + } + + fromName := "Unknown" + if to != 0 { + if from, ok := c.contacts[to]; ok { + fromName = from.name + } + draft.to = to + } + + c.draftsUI.Add(draft.id, fromName, draft.created.Format(shortTimeFormat), indicatorNone) + c.draftsUI.Select(draft.id) + c.drafts[draft.id] = draft + return +} + +func (c *guiClient) composeUI(draft *Draft) interface{} { + if draft == nil { + draft = c.newDraftUI(nil, 0) } var contactNames []string @@ -3267,28 +3297,21 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} } var preSelected string - if inReplyTo != nil { - if from, ok := c.contacts[inReplyTo.from]; ok { - preSelected = from.name - } + if to, ok := c.contacts[draft.to]; ok { + preSelected = to.name } attachments := make(map[uint64]int) detachments := make(map[uint64]int) - - if draft != nil { - if to, ok := c.contacts[draft.to]; ok { - preSelected = to.name - } - for i := range draft.attachments { - attachments[c.randId()] = i - } - for i := range draft.detachments { - detachments[c.randId()] = i - } + for i := range draft.attachments { + attachments[c.randId()] = i + } + for i := range draft.detachments { + detachments[c.randId()] = i } - if draft != nil && draft.inReplyTo != 0 { + var inReplyTo *InboxMessage + if draft.inReplyTo != 0 { for _, msg := range c.inbox { if msg.id == draft.inReplyTo { inReplyTo = msg @@ -3297,27 +3320,6 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} } } - if draft == nil { - from := preSelected - if len(preSelected) == 0 { - from = "Unknown" - } - - draft = &Draft{ - id: c.randId(), - created: c.Now(), - } - if inReplyTo != nil { - draft.inReplyTo = inReplyTo.id - draft.to = inReplyTo.from - draft.body = indentForReply(inReplyTo.message.GetBody()) - } - - c.draftsUI.Add(draft.id, from, draft.created.Format(shortTimeFormat), indicatorNone) - c.draftsUI.Select(draft.id) - c.drafts[draft.id] = draft - } - initialUsageMessage, overSize := draft.usageString() validContactSelected := len(preSelected) > 0 From 28d5b68636cd3510c9e8a63c3eb2d384941d1f9c Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sun, 23 Nov 2014 15:57:55 +0100 Subject: [PATCH 17/36] Send messages to multiple recipients. We replace the to uint64 field in the Draft struct in client.go with two fields of type []uint64 called toNormal and toIntroduce. We clean up these fields when a contact is deleted in client.go as well. Introduction messages get their size approximated in usageString() as well. We modify sendDraft in netwrok.go to send the messages to every user id listed in either toNormal and toIntroduce. All contacts listed in toIntroduce are introduced to all contacts listed in either toNormal or toIntroduce in the sent messages. We make extensive changes to composeUI in gui.go for composing drafts to multiple recipients, optionally introducing some by listing them in toIntroduce instead of toNormal. We display multiple names on the ride hand sidebar whenever possible too. We modify cli.go to correcttly report information about drafts with multiple recipients. We have however deffered adding the ability to edit drafts tor multiple recipients until afer the functionality has been debugged more thuroughly. We leave the introduction functionality in both gui.go and cli.go in tact, but eventually this should be phased into the compose interfaces, which includes editing drafts with no recipients in cli.go. We modify disk.go to support the new Draft struct without changing the existing protobuf, meaning drafts currently lose information about their multiple recipientsr when saved. We address this in a subsequent patch, but seperating any changes to protobufs seems desirable. --- client/cli.go | 166 +++++++++++------- client/client.go | 18 +- client/disk.go | 9 +- client/gui.go | 406 ++++++++++++++++++++++++++++---------------- client/introduce.go | 18 +- client/network.go | 75 ++++++-- 6 files changed, 460 insertions(+), 232 deletions(-) diff --git a/client/cli.go b/client/cli.go index 5f9d5bd..fa6f100 100644 --- a/client/cli.go +++ b/client/cli.go @@ -933,6 +933,13 @@ func (c *cliClient) outboxSummary() (table cliTable) { return } +func (c *client) listDraftRecipients(draft *Draft, nobody string) string { + if len(draft.toNormal) == 0 && len(draft.toIntroduce) == 0 { + return nobody + } + return c.listContactsAndUnknowns(append(draft.toNormal, draft.toIntroduce...)) +} + func (c *cliClient) draftsSummary() (table cliTable) { if len(c.drafts) == 0 { return @@ -951,28 +958,25 @@ func (c *cliClient) draftsSummary() (table cliTable) { rows: make([]cliRow, 0, len(c.drafts)), } - for _, msg := range c.drafts { - if filter != 0 && filter != msg.to { + for _, draft := range c.drafts { + if filter != 0 && !isInIdSet(draft.toNormal, filter) && !isInIdSet(draft.toIntroduce, filter) { continue } - if msg.cliId == invalidCliId { - msg.cliId = c.newCliId() + if draft.cliId == invalidCliId { + draft.cliId = c.newCliId() } - subline := msg.created.Format(shortTimeFormat) - to := "(nobody)" - if msg.to != 0 { - to = c.ContactName(msg.to) - } + subline := draft.created.Format(shortTimeFormat) + toName := c.listDraftRecipients(draft, "(nobody)") table.rows = append(table.rows, cliRow{ indicatorNone, []string{ - terminalEscape(to, false), + terminalEscape(toName, false), subline, }, - msg.cliId, + draft.cliId, }) } @@ -1117,18 +1121,22 @@ func (c *cliClient) processCommand(cmd interface{}) (shouldQuit bool) { switch cmd.(type) { case composeCommand: if contact, ok := c.currentObj.(*Contact); ok { - c.compose(contact, nil, nil) + c.compose(c.newDraftCLI([]uint64{contact.id}, nil, nil)) } else { c.Printf("%s Select contact first\n", termWarnPrefix) } case editCommand: if draft, ok := c.currentObj.(*Draft); ok { - if draft.to == 0 { + if len(draft.toNormal) < 1 { c.Printf("%s Draft was created in the GUI and doesn't have a destination specified. Please use the GUI to manipulate this draft.\n", termErrPrefix) return } - c.compose(nil, draft, nil) + if len(draft.toNormal) > 1 || len(draft.toIntroduce) > 1 { + c.Printf("%s Draft was created in the GUI and has multiple destinations specified. Please use the GUI to manipulate this draft.\n", termErrPrefix) + return + } + c.compose(draft) } else { c.Printf("%s Select draft first\n", termWarnPrefix) } @@ -1143,7 +1151,7 @@ func (c *cliClient) processCommand(cmd interface{}) (shouldQuit bool) { c.Printf("%s Cannot reply to server announcement\n", termWarnPrefix) return } - c.compose(c.contacts[msg.from], nil, msg) + c.compose(c.newDraftCLI([]uint64{msg.from}, nil, msg)) default: goto Handle @@ -1261,11 +1269,11 @@ Handle: case *Contact: c.Printf("%s You attempted to delete a contact (%s). Doing so removes all messages to and from that contact and revokes their ability to send you messages. To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(obj.name, false)) case *Draft: - toName := "" - if obj.to != 0 { - toName = c.ContactName(obj.to) + toName := "" + if len(obj.toNormal) > 0 || len(obj.toIntroduce) > 0 { + toName = " to " + c.listContactsAndUnknowns(append(obj.toNormal, obj.toIntroduce...)) } - c.Printf("%s You attempted to delete a draft message (to %s). To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(toName, false)) + c.Printf("%s You attempted to delete a draft message%s. To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(toName, false)) case *queuedMessage: c.queueMutex.Lock() if c.indexOfQueuedMessage(obj) != -1 { @@ -1308,14 +1316,13 @@ Handle: c.Printf("%s Select draft first\n", termWarnPrefix) return } - if draft.to == 0 { + if len(draft.toNormal) == 0 && len(draft.toIntroduce) == 0 { c.Printf("%s Draft was created in the GUI and doesn't have a destination specified. Please use the GUI to manipulate this draft.\n", termErrPrefix) return } - id, _, err := c.sendDraft(draft) + messages, err := c.sendDraft(draft) if err != nil { c.Printf("%s Error sending: %s\n", termErrPrefix, err) - return } if draft.inReplyTo != 0 { for _, msg := range c.inbox { @@ -1327,17 +1334,18 @@ Handle: } delete(c.drafts, draft.id) c.setCurrentObject(nil) - for _, msg := range c.outbox { - if msg.id == id { - if msg.cliId == invalidCliId { - msg.cliId = c.newCliId() - } - c.Printf("%s Created new outbox entry %s%s%s\n", termInfoPrefix, termCliIdStart, msg.cliId.String(), termReset) + // We previously ranged over c.outbox compairing ids here, but it's safe + // to assume messages contains pointers to the actual outbox messages. + for _, msg := range messages { + if msg.cliId == invalidCliId { + msg.cliId = c.newCliId() + } + c.Printf("%s Created new outbox entry %s%s%s\n", termInfoPrefix, termCliIdStart, msg.cliId.String(), termReset) + if len(messages) == 1 { c.setCurrentObject(msg) - c.showQueueState() - break } } + c.showQueueState() c.save() case abortCommand: @@ -1636,7 +1644,7 @@ Handle: // c.introduceContact_onemany(contact,cl) urls := c.introducePandaMessages_onemany(cl, true) for i := range cl { - draft := c.newDraft(cl[i], nil) + draft := c.newDraft([]uint64{cl[i].id}, nil, nil) draft.cliId = c.newCliId() if i == 0 { draft.body = body0 @@ -1669,7 +1677,7 @@ Handle: urls := c.introducePandaMessages_group(cl, true) for i := range cl { - draft := c.newDraft(cl[i], nil) + draft := c.newDraft([]uint64{cl[i].id}, nil, nil) draft.cliId = c.newCliId() draft.body = body + introducePandaMessageDesc + urls[i] c.sendDraft(draft) @@ -1791,42 +1799,64 @@ func (c *cliClient) inputTextBlock(draft string, isMessage bool) (body string, o return } -func (c *client) newDraft(to *Contact, inReplyTo *InboxMessage) *Draft { +func (c *client) newDraft(toNormal, toIntroduce []uint64, inReplyTo *InboxMessage) *Draft { + // Any recipients specified now overide inReplyTo.from, no panic. draft := &Draft{ - id: c.randId(), - created: time.Now(), - to: to.id, + id: c.randId(), + created: time.Now(), + toNormal: toNormal, + toIntroduce: toIntroduce, } if inReplyTo != nil && inReplyTo.message != nil { draft.inReplyTo = inReplyTo.message.GetId() draft.body = indentForReply(inReplyTo.message.GetBody()) + if len(toNormal) == 0 && len(toIntroduce) == 0 && inReplyTo.from != 0 { + toNormal = []uint64{inReplyTo.from} + } } c.drafts[draft.id] = draft return draft } -func (c *cliClient) compose(to *Contact, draft *Draft, inReplyTo *InboxMessage) { +func (c *cliClient) newDraftCLI(toNormal, toIntroduce []uint64, inReplyTo *InboxMessage) *Draft { + draft := c.newDraft(toNormal, toIntroduce, inReplyTo) + draft.cliId = c.newCliId() + c.Printf("%s Created new draft: %s%s%s\n", termInfoPrefix, termCliIdStart, draft.cliId.String(), termReset) + c.setCurrentObject(draft) + return draft +} + +func (c *cliClient) compose(draft *Draft) { if draft == nil { - draft = c.newDraft(to, inReplyTo) - draft.cliId = c.newCliId() - c.Printf("%s Created new draft: %s%s%s\n", termInfoPrefix, termCliIdStart, draft.cliId.String(), termReset) - c.setCurrentObject(draft) + c.Printf("%s Internal error, compose nolonger initializes drafts.\n", termErrPrefix) } - if to == nil { - to = c.contacts[draft.to] + + body0 := "" + funTo := func(title string, tos []uint64) bool { + if len(tos) == 0 { + return true + } + body0 += title + c.listContactsAndUnknowns(tos) + "\n" + // TODO : Allow writing messages to pending contacts, issue warning here + for _, to := range tos { + if c.contacts[to].isPending { + c.Printf("%s Cannot send message to pending contact %s.\n", termErrPrefix, c.contacts[to].name) + return false + } + } + return true } - if to.isPending { - c.Printf("%s Cannot send message to pending contact\n", termErrPrefix) + if !funTo("Introdiucing: ", draft.toIntroduce) || + !funTo("To: ", draft.toNormal) { return } - body, ok := c.inputTextBlock(fmt.Sprintf("To: %s\n\n"+draft.body, to.name), true) + body, ok := c.inputTextBlock(body0+"\n"+draft.body, true) if !ok { return } draft.body = body c.printDraftSize(draft) - c.save() } @@ -1931,27 +1961,40 @@ func (c *cliClient) showOutbox(msg *queuedMessage) { c.Printf("\n") } -func (c *cliClient) showDraft(msg *Draft) { - to := "(not specified)" - if msg.to != 0 { - to = c.ContactName(msg.to) +func (c *cliClient) showDraft(draft *Draft) { + toLine := "" + if len(draft.toIntroduce) > 0 { + toLine = fmt.Sprintf("%s Introdiucing: %s\n", termHeaderPrefix, + terminalEscape(c.listContactsAndUnknowns(draft.toIntroduce), false)) + } + if len(draft.toNormal) > 0 { + also := "" + if len(toLine) > 0 { + also = "Also " + } + toLine += fmt.Sprintf("%s %sTo: %s\n", termHeaderPrefix, also, + terminalEscape(c.listContactsAndUnknowns(draft.toNormal), false)) + } + if len(toLine) == 0 { + toLine = fmt.Sprintf("%s To: %s\n", termHeaderPrefix, "(not specified)") } - c.Printf("%s To: %s\n", termHeaderPrefix, terminalEscape(to, false)) - c.Printf("%s Created: %s\n", termHeaderPrefix, formatTime(msg.created)) - if len(msg.attachments) > 0 { + c.Printf(toLine) + + c.Printf("%s Created: %s\n", termHeaderPrefix, formatTime(draft.created)) + if len(draft.attachments) > 0 { c.Printf("%s Attachments (use 'remove <#>' to remove):\n", termHeaderPrefix) } - for i, attachment := range msg.attachments { + for i, attachment := range draft.attachments { c.Printf("%s %d: %s (%d bytes):\n", termHeaderPrefix, i+1, terminalEscape(attachment.GetFilename(), false), len(attachment.Contents)) } - if len(msg.detachments) > 0 { + if len(draft.detachments) > 0 { c.Printf("%s Detachments (use 'remove <#>' to remove):\n", termHeaderPrefix) } - for i, detachment := range msg.detachments { - c.Printf("%s %d: %s (%d bytes):\n", termHeaderPrefix, 1+len(msg.attachments)+i, terminalEscape(detachment.GetFilename(), false), detachment.GetSize()) + for i, detachment := range draft.detachments { + c.Printf("%s %d: %s (%d bytes):\n", termHeaderPrefix, 1+len(draft.attachments)+i, terminalEscape(detachment.GetFilename(), false), detachment.GetSize()) } c.Printf("\n") - c.term.Write([]byte(terminalEscape(string(msg.body), true /* line breaks ok */))) + c.term.Write([]byte(terminalEscape(string(draft.body), true /* line breaks ok */))) c.Printf("\n") } @@ -2033,7 +2076,10 @@ func (c *client) listContactsAndUnknowns(ids []uint64) string { } } if unknowns > 0 { - listing += fmt.Sprintf("and %d unknown contacts.", unknowns) + if len(listing) > 0 { + listing += "and " + } + listing += fmt.Sprintf("%d unknown contacts.", unknowns) } listing = strings.TrimSuffix(listing, ", ") return listing diff --git a/client/client.go b/client/client.go index d276048..b5f2eb2 100644 --- a/client/client.go +++ b/client/client.go @@ -570,7 +570,8 @@ type pendingDetachment struct { type Draft struct { id uint64 created time.Time - to uint64 + toNormal []uint64 + toIntroduce []uint64 body string inReplyTo uint64 attachments []*pond.Message_Attachment @@ -630,8 +631,14 @@ func (draft *Draft) usageString() (string, bool) { if err != nil { panic("error while serialising candidate Message: " + err.Error()) } + l := uint64(len(serialized)) - s := fmt.Sprintf("%s of %s bytes", prettyNumber(uint64(len(serialized))), prettyNumber(pond.MaxSerializedMessage)) + // We must overestimate the size of introductions + to := append(draft.toNormal, draft.toIntroduce...) + expectedContactNameSize := 30 + l += uint64((len(to) - 1) * (len("pond-introduce-panda://") + 64 + expectedContactNameSize)) + + s := fmt.Sprintf("%s of %s bytes", prettyNumber(l), prettyNumber(pond.MaxSerializedMessage)) return s, len(serialized) > pond.MaxSerializedMessage } @@ -679,7 +686,7 @@ func (c *client) outboxToDraft(msg *queuedMessage) *Draft { draft := &Draft{ id: msg.id, created: msg.created, - to: msg.to, + toNormal: []uint64{msg.to}, body: string(msg.message.Body), attachments: msg.message.Files, detachments: msg.message.DetachedFiles, @@ -1223,9 +1230,8 @@ func (c *client) deleteContact(contact *Contact) { c.inbox = newInbox for _, draft := range c.drafts { - if draft.to == contact.id { - draft.to = 0 - } + removeIdSet(&draft.toNormal, contact.id) + removeIdSet(&draft.toIntroduce, contact.id) } for id, contact := range c.contacts { diff --git a/client/disk.go b/client/disk.go index efd7549..4519aba 100644 --- a/client/disk.go +++ b/client/disk.go @@ -251,7 +251,8 @@ func (c *client) unmarshal(state *disk.State) error { } c.registerId(draft.id) if m.To != nil { - draft.to = *m.To + // draft.to + draft.toNormal = []uint64{*m.To} } if m.InReplyTo != nil { draft.inReplyTo = *m.InReplyTo @@ -384,9 +385,11 @@ func (c *client) marshal() []byte { Detachments: draft.detachments, Created: proto.Int64(draft.created.Unix()), } - if draft.to != 0 { - m.To = proto.Uint64(draft.to) + // draft.to + if len(draft.toNormal) > 0 { + m.To = proto.Uint64(draft.toNormal[0]) } + if draft.inReplyTo != 0 { m.InReplyTo = proto.Uint64(draft.inReplyTo) } diff --git a/client/gui.go b/client/gui.go index 3f5d74d..db9a3b7 100644 --- a/client/gui.go +++ b/client/gui.go @@ -389,6 +389,10 @@ func (c *guiClient) processMessageDelivered(msg *queuedMessage) { c.outboxUI.SetIndicator(msg.id, indicatorYellow) } +func (c *guiClient) sideDraftRecipients(draft *Draft) string { + return maybeTruncate(c.listDraftRecipients(draft, "Unknown")) +} + func (c *guiClient) mainUI() { ui := Paned{ left: Scrolled{ @@ -595,12 +599,9 @@ func (c *guiClient) mainUI() { } for _, draft := range c.drafts { - to := "Unknown" - if draft.to != 0 { - to = c.ContactName(draft.to) - } + toLine := c.sideDraftRecipients(draft) subline := draft.created.Format(shortTimeFormat) - c.draftsUI.Add(draft.id, to, subline, indicatorNone) + c.draftsUI.Add(draft.id, toLine, subline, indicatorNone) } c.clientUI = &listUI{ @@ -1662,7 +1663,7 @@ NextEvent: c.gui.Signal() case click.name == "reply": c.inboxUI.Deselect() - return c.composeUI(c.newDraftUI(msg, 0)) + return c.composeUI(c.newDraftUI(nil, nil, msg)) case click.name == "delete": c.inboxUI.Remove(msg.id) c.deleteInboxMsg(msg.id) @@ -2297,14 +2298,9 @@ func (c *guiClient) showContact(id uint64) interface{} { c.outboxUI.SetLine(msg.id, newName) } } - for _, msg := range c.drafts { - if msg.to == contact.id { - c.draftsUI.SetLine(msg.id, newName) - } - } - for _, msg := range c.drafts { - if msg.to == contact.id { - c.draftsUI.SetLine(msg.id, newName) + for _, draft := range c.drafts { + if isInIdSet(draft.toNormal, contact.id) || isInIdSet(draft.toIntroduce, contact.id) { + c.draftsUI.SetLine(draft.id, c.sideDraftRecipients(draft)) } } c.contactsUI.SetLine(contact.id, newName) @@ -2514,7 +2510,7 @@ func (c *guiClient) introduceUI(id uint64) interface{} { urls = c.introducePandaMessages_group(cl, true) } for i := range cl { - draft := c.newDraft(cl[i], nil) + draft := c.newDraft(nil, []uint64{cl[i].id}, nil) draft.body = messageBody + introducePandaMessageDesc + urls[i] c.sendDraft(draft) c.log.Printf("Queued introduction message for %s.", cl[i].name) @@ -3240,65 +3236,19 @@ func (c *guiClient) maybeProcessDetachmentMsg(event interface{}, ui DetachmentUI return false } -func (c *guiClient) updateUsage(validContactSelected bool, draft *Draft) bool { - usageMessage, over := draft.usageString() - c.gui.Actions() <- SetText{name: "usage", text: usageMessage} - color := uint32(colorBlack) - if over { - color = colorRed - c.gui.Actions() <- Sensitive{name: "send", sensitive: false} - } else if validContactSelected { - c.gui.Actions() <- Sensitive{name: "send", sensitive: true} - } - c.gui.Actions() <- SetForeground{name: "usage", foreground: color} - return over -} - -func (c *guiClient) newDraftUI(inReplyTo *InboxMessage, to uint64) (draft *Draft) { - if inReplyTo != nil && to != 0 { - panic("newDraftUI : inReplyTo and to both set") - } - - draft = &Draft{ - id: c.randId(), - created: c.Now(), - } - - if inReplyTo != nil { - draft.inReplyTo = inReplyTo.id - to = inReplyTo.from - draft.body = indentForReply(inReplyTo.message.GetBody()) - } - - fromName := "Unknown" - if to != 0 { - if from, ok := c.contacts[to]; ok { - fromName = from.name - } - draft.to = to - } - - c.draftsUI.Add(draft.id, fromName, draft.created.Format(shortTimeFormat), indicatorNone) +func (c *guiClient) newDraftUI(toNormal, toIntroduce []uint64, inReplyTo *InboxMessage) *Draft { + draft := c.newDraft(toNormal, toIntroduce, inReplyTo) + // If the reply has selected text, then the caller should change draft.body + // because we default to quoting the whole reply in newDraft + c.draftsUI.Add(draft.id, c.sideDraftRecipients(draft), + draft.created.Format(shortTimeFormat), indicatorNone) c.draftsUI.Select(draft.id) - c.drafts[draft.id] = draft - return + return draft } func (c *guiClient) composeUI(draft *Draft) interface{} { if draft == nil { - draft = c.newDraftUI(nil, 0) - } - - var contactNames []string - for _, contact := range c.contacts { - if !contact.isPending && !contact.revokedUs { - contactNames = append(contactNames, contact.name) - } - } - - var preSelected string - if to, ok := c.contacts[draft.to]; ok { - preSelected = to.name + draft = c.newDraftUI(nil, nil, nil) } attachments := make(map[uint64]int) @@ -3310,36 +3260,30 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { detachments[c.randId()] = i } - var inReplyTo *InboxMessage - if draft.inReplyTo != 0 { - for _, msg := range c.inbox { - if msg.id == draft.inReplyTo { - inReplyTo = msg - break - } - } - } - - initialUsageMessage, overSize := draft.usageString() - validContactSelected := len(preSelected) > 0 + // We modify overSize in updateSend() and updateUsage() + // We modify usageMessage in updateUsage() but do not use it currently + usageMessage, overSize := draft.usageString() lhs := VBox{ children: []Widget{ HBox{ widgetBase: widgetBase{padding: 2}, children: []Widget{ - Label{ - widgetBase: widgetBase{font: fontMainLabel, foreground: colorHeaderForeground, padding: 10}, - text: "TO", - yAlign: 0.5, - }, - Combo{ - widgetBase: widgetBase{ - name: "to", - insensitive: len(preSelected) > 0 && inReplyTo != nil, + VBox{ + widgetBase: widgetBase{}, + children: []Widget{ + Label{ + widgetBase: widgetBase{font: fontMainLabel, foreground: colorHeaderForeground, padding: 10}, + text: "TO", + yAlign: 0.5, + }, + Label{ + widgetBase: widgetBase{expand: true, fill: true}, + }, }, - labels: contactNames, - preSelected: preSelected, + }, + VBox{ + widgetBase: widgetBase{name: "to-box", padding: 0}, }, }, }, @@ -3353,7 +3297,7 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { }, Label{ widgetBase: widgetBase{name: "usage"}, - text: initialUsageMessage, + text: usageMessage, }, }, }, @@ -3385,7 +3329,7 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { widgetBase: widgetBase{padding: 5}, children: []Widget{ Button{ - widgetBase: widgetBase{name: "send", insensitive: !validContactSelected, padding: 2}, + widgetBase: widgetBase{name: "send", insensitive: true, padding: 2}, text: "Send", }, Button{ @@ -3439,6 +3383,127 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { c.gui.Actions() <- SetChild{name: "right", child: ui} + toBoxName := func(s string, i uint64) string { + return fmt.Sprintf("to-box-%s-%x", s, i) + } + toBoxAddEntry := func(id uint64, introduce bool) { + if introduce { + addIdSet(&draft.toIntroduce, id) + removeIdSet(&draft.toNormal, id) + } else { + addIdSet(&draft.toNormal, id) + removeIdSet(&draft.toIntroduce, id) + } + c.gui.Actions() <- Append{ + name: "to-box", + children: []Widget{ + HBox{ + widgetBase: widgetBase{name: toBoxName("entry", id)}, + children: []Widget{ + Entry{ + widgetBase: widgetBase{insensitive: true}, + text: c.contacts[id].name, + }, + Button{ + widgetBase: widgetBase{name: toBoxName("remove", id), font: "Liberation Sans 8"}, + image: indicatorRemove, + }, + CheckButton{ + widgetBase: widgetBase{ + name: toBoxName("introduce", id), + padding: 10, + }, + checked: introduce, + text: "Introduce", + }, + }, + }, + }, + } + } + originalToIntroduce := draft.toIntroduce + draft.toIntroduce = nil + originalToNormal := draft.toNormal + draft.toNormal = nil + for _, id := range originalToIntroduce { + toBoxAddEntry(id, true) + } + for _, id := range originalToNormal { + toBoxAddEntry(id, false) + } + // Should we panic here if draft.toNormal != originalToNormal or + // draft.toIntroduce != originalToIntroduce? + + var toBoxLines uint64 = 1 // zero signifies that no combo box exists + toBoxAddCombo := func() { + var contactNames []string + for _, contact := range c.contacts { + if !contact.isPending && !contact.revokedUs && + !isInIdSet(draft.toNormal, contact.id) && + !isInIdSet(draft.toIntroduce, contact.id) { + contactNames = append(contactNames, contact.name) + } + } + if len(contactNames) == 0 { + toBoxLines = 0 + return + } + more := "" + if len(draft.toNormal)+len(draft.toIntroduce) > 0 { + more = "+" + } + c.gui.Actions() <- Append{ + name: "to-box", + children: []Widget{ + HBox{ + widgetBase: widgetBase{name: toBoxName("adder", toBoxLines)}, + children: []Widget{ + Label{ + widgetBase: widgetBase{padding: 10}, + text: more, + yAlign: 0.5, + }, + Combo{ + widgetBase: widgetBase{name: "to-box-add"}, + labels: contactNames, + }, + }, + }, + }, + } + } + toBoxAddCombo() + toBoxUpdateCombo := func() { + if toBoxLines > 0 { + c.gui.Actions() <- Destroy{name: toBoxName("adder", toBoxLines)} + } + toBoxLines++ + toBoxAddCombo() + } + + updateSend := func() { + sendable := len(draft.toNormal) > 0 || len(draft.toIntroduce) > 1 + for _, id := range append(draft.toNormal, draft.toIntroduce...) { + if c.contacts[id].isPending || c.contacts[id].revokedUs { + c.gui.Actions() <- SetForeground{name: toBoxName("remove", id), foreground: colorRed} + sendable = false + } + } + c.gui.Actions() <- Sensitive{name: "send", sensitive: sendable && !overSize} + } + updateSend() + + // We should probably just remove introduceSensitivity() because + // the Introduce setting is copied after the first line. + introduceSensitivity := func() { + /* + if len(draft.toNormal) == 0 { return } + c.gui.Actions() <- Sensitive{name: toBoxName("introduce",draft.toNormal[0]), + sensitive: len(draft.toNormal) > 1 || len(draft.toIntroduce) > 0 } + */ + } + introduceSensitivity() + if draft.pendingDetachments == nil { draft.pendingDetachments = make(map[uint64]*pendingDetachment) } @@ -3469,9 +3534,18 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { } } - detachmentUI := ComposeDetachmentUI{draft, detachments, c.gui, func() { - overSize = c.updateUsage(validContactSelected, draft) - }} + updateUsage := func() { + usageMessage, overSize = draft.usageString() + c.gui.Actions() <- SetText{name: "usage", text: usageMessage} + color := uint32(colorBlack) + if overSize { + color = colorRed + } + c.gui.Actions() <- SetForeground{name: "usage", foreground: color} + updateSend() + } + + detachmentUI := ComposeDetachmentUI{draft, detachments, c.gui, updateUsage} c.gui.Actions() <- UIState{uiStateCompose} c.gui.Signal() @@ -3483,7 +3557,7 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { } if update, ok := event.(Update); ok { - overSize = c.updateUsage(validContactSelected, draft) + updateUsage() draft.body = update.text c.gui.Signal() continue @@ -3553,7 +3627,7 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { widgetForAttachment(id, label, err != nil, extraWidgets), }, } - overSize = c.updateUsage(validContactSelected, draft) + updateUsage() c.gui.Signal() } if open, ok := event.(OpenResult); ok && open.ok && open.arg != nil { @@ -3582,38 +3656,76 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { if !ok { continue } - if click.name == "attach" { - c.gui.Actions() <- FileOpen{ - title: "Attach File", + if click.name == "discard" { + c.draftsUI.Remove(draft.id) + delete(c.drafts, draft.id) + c.save() + c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} + c.gui.Actions() <- UIState{uiStateMain} + c.gui.Signal() + return nil + } + + // Recipients interface + if click.name == "to-box-add" { + name := click.combos["to-box-add"] + if len(name) == 0 { + continue } + contact, ok := c.contactByName(name) + if !ok { + panic("unreachable") + } + introduce := len(draft.toIntroduce) > 0 && len(draft.toNormal) == 0 + toBoxAddEntry(contact.id, introduce) + toBoxUpdateCombo() + updateSend() + introduceSensitivity() + c.draftsUI.SetLine(draft.id, c.sideDraftRecipients(draft)) c.gui.Signal() continue } - if click.name == "to" { - selected := click.combos["to"] - if len(selected) > 0 { - validContactSelected = true + const toRemovePrefix = "to-box-remove-" + if strings.HasPrefix(click.name, toRemovePrefix) { + id, err := strconv.ParseUint(click.name[len(toRemovePrefix):], 16, 64) + if _, ok := c.contacts[id]; err != nil || !ok { + panic(click.name) } - for _, contact := range c.contacts { - if contact.name == selected { - draft.to = contact.id - } + removeIdSet(&draft.toNormal, id) + removeIdSet(&draft.toIntroduce, id) + c.gui.Actions() <- Destroy{name: toBoxName("entry", id)} + toBoxUpdateCombo() + updateSend() + introduceSensitivity() + c.draftsUI.SetLine(draft.id, c.sideDraftRecipients(draft)) + c.gui.Signal() + continue + } + const toIntroducePrefix = "to-box-introduce-" + if strings.HasPrefix(click.name, toIntroducePrefix) { + id, err := strconv.ParseUint(click.name[len(toIntroducePrefix):], 16, 64) + if _, ok := c.contacts[id]; err != nil || !ok { + panic(click.name) } - c.draftsUI.SetLine(draft.id, selected) - if validContactSelected && !overSize { - c.gui.Actions() <- Sensitive{name: "send", sensitive: true} - c.gui.Signal() + introduce := click.checks[click.name] + if introduce { + addIdSet(&draft.toIntroduce, id) + removeIdSet(&draft.toNormal, id) + } else { + addIdSet(&draft.toNormal, id) + removeIdSet(&draft.toIntroduce, id) } + // c.gui.Signal() continue } - if click.name == "discard" { - c.draftsUI.Remove(draft.id) - delete(c.drafts, draft.id) - c.save() - c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} - c.gui.Actions() <- UIState{uiStateMain} + + // Attachment interface + if click.name == "attach" { + c.gui.Actions() <- FileOpen{ + title: "Attach File", + } c.gui.Signal() - return nil + continue } if strings.HasPrefix(click.name, "remove-") { // One of the attachment remove buttons. @@ -3636,7 +3748,7 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { draft.detachments = append(draft.detachments[:index], draft.detachments[index+1:]...) delete(detachments, id) } - overSize = c.updateUsage(validContactSelected, draft) + updateUsage() c.gui.Signal() continue } @@ -3681,44 +3793,46 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { if click.name != "send" { continue } + // if len(click.combos["to-box-add"]) > 0 { panic(click.name) } - toName := click.combos["to"] - if len(toName) == 0 { - continue - } - for _, contact := range c.contacts { - if contact.name == toName { - draft.to = contact.id - break - } - } - - if inReplyTo != nil { - draft.inReplyTo = inReplyTo.message.GetId() - } draft.body = click.textViews["body"] - id, created, err := c.sendDraft(draft) + messages, err := c.sendDraft(draft) if err != nil { // TODO: handle this case better. println(err.Error()) c.log.Errorf("Error sending message: %s", err) continue } - to := c.contacts[draft.to] - c.outboxUI.Add(id, to.name, created.Format(shortTimeFormat), indicatorRed) - if inReplyTo != nil { - inReplyTo.acked = true - c.inboxUI.SetIndicator(inReplyTo.id, indicatorNone) + + for _, msg := range messages { + c.outboxUI.Add(msg.id, c.ContactName(msg.to), + msg.created.Format(shortTimeFormat), indicatorRed) + } + + if draft.inReplyTo != 0 { + for _, msg := range c.inbox { + if msg.id == draft.inReplyTo { + msg.acked = true + c.inboxUI.SetIndicator(msg.id, indicatorNone) + break + } + } } c.draftsUI.Remove(draft.id) delete(c.drafts, draft.id) - c.save() - - c.outboxUI.Select(id) - return c.showOutbox(id) + if len(messages) == 1 { + id := messages[0].id + c.outboxUI.Select(id) + return c.showOutbox(id) + } else { + c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} + c.gui.Actions() <- UIState{uiStateMain} + c.gui.Signal() + return nil + } } return nil diff --git a/client/introduce.go b/client/introduce.go index 3ba853b..42a8fb9 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -9,7 +9,7 @@ import ( ) const ( - introducePandaMessageDesc = "Introduction URLs for proposed new contacts :\n" + introducePandaMessageDesc = "\n---- Introduction URIs for proposed new contacts ----\n" ) func addIdSet(set *[]uint64, id uint64) { @@ -30,6 +30,22 @@ func removeIdSet(set *[]uint64, id uint64) { } } +func isInIdSet(set []uint64, id uint64) bool { + for _, s := range set { + if s == id { + return true + } + } + return false +} + +func (c *client) contactListFromIdSet(set []uint64) (ci contactList) { + for _, id := range set { + ci = append(ci, c.contacts[id]) + } + return +} + func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (string, string) { panda_secret := panda.NewSecretString(c.rand)[2:] s := func(cnt *Contact) string { diff --git a/client/network.go b/client/network.go index 673e3c0..8a2d58b 100644 --- a/client/network.go +++ b/client/network.go @@ -71,7 +71,7 @@ func (c *client) sendAck(msg *InboxMessage) { } id := c.randId() - err := c.send(to, &pond.Message{ + message := &pond.Message{ Id: proto.Uint64(id), Time: proto.Int64(time.Now().Unix()), Body: make([]byte, 0), @@ -79,23 +79,28 @@ func (c *client) sendAck(msg *InboxMessage) { MyNextDh: myNextDH, InReplyTo: msg.message.Id, SupportedVersion: proto.Int32(protoVersion), - }) - if err != nil { - c.log.Errorf("Error sending message: %s", err) } + if err := c.sendTest(message); err != nil { + c.log.Errorf("Error sending ACK message: %s", err) + return + } + c.send(to, message) } -// send encrypts |message| and enqueues it for transmission. -func (c *client) send(to *Contact, message *pond.Message) error { +// Verify that no errors will occur when enqueuing user created messages +func (c *client) sendTest(message *pond.Message) error { messageBytes, err := proto.Marshal(message) if err != nil { return err } - if len(messageBytes) > pond.MaxSerializedMessage { return errors.New("message too large") } + return nil +} +// send encrypts |message| and enqueues it for transmission. +func (c *client) send(to *Contact, message *pond.Message) *queuedMessage { out := &queuedMessage{ id: *message.Id, to: to.id, @@ -105,29 +110,28 @@ func (c *client) send(to *Contact, message *pond.Message) error { } c.enqueue(out) c.outbox = append(c.outbox, out) - - return nil + return out } -func (c *client) sendDraft(draft *Draft) (uint64, time.Time, error) { - to := c.contacts[draft.to] - +func (c *client) sendDraftTo(draft *Draft, to *Contact) (*queuedMessage, error) { // Zero length bodies are ACKs. if len(draft.body) == 0 { draft.body = " " } id := c.randId() - created := c.Now() message := &pond.Message{ Id: proto.Uint64(id), - Time: proto.Int64(created.Unix()), + Time: proto.Int64(c.Now().Unix()), Body: []byte(draft.body), BodyEncoding: pond.Message_RAW.Enum(), Files: draft.attachments, DetachedFiles: draft.detachments, SupportedVersion: proto.Int32(protoVersion), } + if err := c.sendTest(message); err != nil { + return nil, err + } if r := draft.inReplyTo; r != 0 { message.InReplyTo = proto.Uint64(r) @@ -139,8 +143,47 @@ func (c *client) sendDraft(draft *Draft) (uint64, time.Time, error) { message.MyNextDh = nextDHPub[:] } - err := c.send(to, message) - return id, created, err + return c.send(to, message), nil +} + +func (c *client) sendDraft(draft *Draft) ([]*queuedMessage, error) { + var outs []*queuedMessage + var outs_bad []*queuedMessage + // var outs_err []error + + body := draft.body + urlsIntroduce, urlsNormal := c.introducePandaMessages( + c.contactListFromIdSet(draft.toIntroduce), + c.contactListFromIdSet(draft.toNormal), true) + urls := append(urlsIntroduce, urlsNormal...) + for i, to := range append(draft.toIntroduce, draft.toNormal...) { + if len(draft.toIntroduce) > 0 { + draft.body = body + introducePandaMessageDesc + urls[i] + } + out, err := c.sendDraftTo(draft, c.contacts[to]) + if err != nil { + if i == 0 { + return nil, err + } else { + outs_bad = append(outs_bad, out) + // outs_err = append(outs_err,err) + continue + } + } + outs = append(outs, out) + } + draft.body = body + + if len(outs_bad) == 0 { + return outs, nil + } + // We could theoretically just call sendTest first thing in sendDraft, + // meaning this should be unreachable, but panic gracefully here anyways. + for _, out := range outs_bad { + c.outboxToDraft(out) + } + c.save() + panic("Only partially enqueued multi-recipient message failed") } // tooLarge returns true if the given message is too large to serialise. From eab3e7cddcd1f82c440f6574df759ffba2860bf2 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sat, 13 Dec 2014 22:27:22 -0500 Subject: [PATCH 18/36] Fix handling of Draft.inReplyTo There was an inconsistency in what Draft.inReplyTo meant between the GUI and CLI, a minor bug for people who used both the CLI and GUI, which led me to break testReplyACKs when adding more homogenious code. --- client/cli.go | 2 +- client/gui.go | 1 + client/network.go | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/cli.go b/client/cli.go index fa6f100..3221b6d 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1808,7 +1808,7 @@ func (c *client) newDraft(toNormal, toIntroduce []uint64, inReplyTo *InboxMessag toIntroduce: toIntroduce, } if inReplyTo != nil && inReplyTo.message != nil { - draft.inReplyTo = inReplyTo.message.GetId() + draft.inReplyTo = inReplyTo.id draft.body = indentForReply(inReplyTo.message.GetBody()) if len(toNormal) == 0 && len(toIntroduce) == 0 && inReplyTo.from != 0 { toNormal = []uint64{inReplyTo.from} diff --git a/client/gui.go b/client/gui.go index db9a3b7..ff4aa58 100644 --- a/client/gui.go +++ b/client/gui.go @@ -1661,6 +1661,7 @@ NextEvent: c.inboxUI.SetIndicator(msg.id, indicatorNone) c.gui.Actions() <- UIState{uiStateInbox} c.gui.Signal() + c.save() case click.name == "reply": c.inboxUI.Deselect() return c.composeUI(c.newDraftUI(nil, nil, msg)) diff --git a/client/network.go b/client/network.go index 8a2d58b..538fc66 100644 --- a/client/network.go +++ b/client/network.go @@ -134,7 +134,13 @@ func (c *client) sendDraftTo(draft *Draft, to *Contact) (*queuedMessage, error) } if r := draft.inReplyTo; r != 0 { - message.InReplyTo = proto.Uint64(r) + for _, msg := range c.inbox { + if msg.id == draft.inReplyTo { + r = msg.message.GetId() + message.InReplyTo = proto.Uint64(r) + break + } + } } if to.ratchet == nil { From 434107415d03ba74bca1f1204ece67121a431fa8 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Tue, 25 Nov 2014 16:40:35 +0100 Subject: [PATCH 19/36] Add a SetChecked action to our GTK support --- client/abstractgui.go | 5 +++++ client/gtk.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/client/abstractgui.go b/client/abstractgui.go index 4e02dc7..3949353 100644 --- a/client/abstractgui.go +++ b/client/abstractgui.go @@ -279,6 +279,11 @@ type Sensitive struct { sensitive bool } +type SetChecked struct { + name string + checked bool +} + type SetBackground struct { name string color uint32 diff --git a/client/gtk.go b/client/gtk.go index 96f707f..9cfbb89 100644 --- a/client/gtk.go +++ b/client/gtk.go @@ -580,6 +580,9 @@ func (ui *GTKUI) handle(action interface{}) { case Sensitive: widget := gtk.GtkWidget{ui.getWidget(action.name).ToNative()} widget.SetSensitive(action.sensitive) + case SetChecked: + widget := gtk.GtkToggleButton{gtk.GtkButton{gtk.GtkBin{gtk.GtkContainer{gtk.GtkWidget{ui.getWidget(action.name).ToNative()}}}}} + widget.SetActive(action.checked) case StartSpinner: widget := gtk.GtkSpinner{gtk.GtkWidget{ui.getWidget(action.name).ToNative()}} widget.Start() From 140f0469ab837d0336338af6f8072961fe642582 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 24 Nov 2014 11:33:28 +0100 Subject: [PATCH 20/36] Simpler introduceUI that employs composeUI for sending introductions. Adds an information button to explain how introductions work to composeUI too. --- client/gui.go | 453 +++++++++++++++++++++++++++----------------------- 1 file changed, 243 insertions(+), 210 deletions(-) diff --git a/client/gui.go b/client/gui.go index ff4aa58..5c21655 100644 --- a/client/gui.go +++ b/client/gui.go @@ -36,6 +36,7 @@ const ( colorTitleForeground = 0xdddddd colorBlack = 1 colorRed = 0xff0000 + colorBlue = 0x0000ff colorError = 0xff0000 colorImminently = 0xffdddd colorDeleteSoon = 0xdddddd @@ -670,7 +671,7 @@ func (c *guiClient) mainUI() { case "newcontact": nextEvent = c.newContactUI(nil) case "introduce": - nextEvent = c.introduceUI(0) + nextEvent = c.introduceUI(nil) case "compose": nextEvent = c.composeUI(nil) } @@ -2206,6 +2207,13 @@ func (c *guiClient) showContact(id uint64) interface{} { }, {{1, 1, nil}}, { + {1, 1, Button{ + widgetBase: widgetBase{ + name: "composeTo", + insensitive: contact.isPending || contact.revokedUs, + }, + text: "Compose", + }}, {1, 1, Button{ widgetBase: widgetBase{ name: "introduceTo", @@ -2309,221 +2317,198 @@ func (c *guiClient) showContact(id uint64) interface{} { c.gui.Signal() c.save() + case "composeTo": + return c.composeUI(c.newDraftUI([]uint64{contact.id}, nil, nil)) case "introduceTo": - return c.introduceUI(contact.id) + return c.introduceUI(c.newDraftUI(nil, []uint64{contact.id}, nil)) } } panic("unreachable") } -func (c *guiClient) introduceUI(id uint64) interface{} { - const ( - contactCheckBoxPrefix = "contactchecked-" - ) +func (c *guiClient) introduceUI(draft *Draft) interface{} { + if draft == nil { + draft = c.newDraftUI(nil, nil, nil) + c.draftsUI.SetLine(draft.id, "Introduction") + } - var contactIds []uint64 - var contactLabels []string - var contactChecks []bool - var contactsBoxes [][]GridE - var messageBody string - var messageCompose = []GridE{ - {1, 1, nil}, - {10, 1, Scrolled{ - widgetBase: widgetBase{expand: true, fill: true}, - horizontal: true, - child: TextView{ - widgetBase: widgetBase{expand: true, fill: true, name: "body"}, - editable: true, - wrap: true, - updateOnChange: true, - spellCheck: true, - text: messageBody, - }, - }}, - {1, 1, Button{ - widgetBase: widgetBase{width: 40, name: "doIntroduce"}, - text: "Introduce", - }}, - } - var contactsBoxesLine []GridE - contactsBoxesLine = []GridE{{1, 1, nil}} - var i int - i = 0 - for _, contact := range c.contacts { + type contactCheckBoxes struct { + prefix string + set *[]uint64 + } + toNormal := contactCheckBoxes{ + prefix: "to-normal-", + set: &draft.toNormal, + } + toIntroduce := contactCheckBoxes{ + prefix: "to-introduce-", + set: &draft.toIntroduce, + } + anti := func(to *contactCheckBoxes) *contactCheckBoxes { + if to == &toNormal { + return &toIntroduce + } + if to == &toIntroduce { + return &toNormal + } + panic("bad pointer") + return nil + } + + // Iterating over c.contacts returns contacts in different orders. + nameLen := 0 + var cl contactList + for _, contact := range c.contactsSorted() { if contact.isPending || contact.revokedUs { continue } - contactLabels = append(contactLabels, contact.name) - contactIds = append(contactIds, contact.id) - contactChecks = append(contactChecks, false) - contactsBoxesLine = append(contactsBoxesLine, - GridE{3, 1, CheckButton{ - widgetBase: widgetBase{ - name: fmt.Sprintf("%s%d", contactCheckBoxPrefix, i), - }, - checked: false, - text: contact.name, - }}, - GridE{1, 1, nil}) - if i%3 == 2 { - contactsBoxes = append(contactsBoxes, contactsBoxesLine) - contactsBoxesLine = []GridE{{1, 1, nil}} + cl = append(cl, contact) + if len(contact.name) > nameLen { + nameLen = len(contact.name) } - i++ } - contactsBoxes = append(contactsBoxes, contactsBoxesLine) + var perLine int = 100 / (nameLen + 5) + if perLine > 10 { + perLine = 10 + } - var preSelected string - if id != 0 { - if c.contacts[id].isPending || c.contacts[id].revokedUs { - id = 0 - } else { - preSelected = c.contacts[id].name + ccbBuild := func(to *contactCheckBoxes) Grid { + var lineStart []GridE /* []GridE{ {1,1,nil} } */ + var lineSep GridE /* GridE{ {1,1,nil} } */ + var lines [][]GridE + var line []GridE = lineStart + var i int = 0 + for _, contact := range cl { + line = append(line, + GridE{1, 1, CheckButton{ + widgetBase: widgetBase{ + name: fmt.Sprintf("%s%x", to.prefix, contact.id), + padding: 2, + }, + checked: isInIdSet(*to.set, contact.id), + text: contact.name, + }}, lineSep) + if i%perLine == perLine-1 { + lines = append(lines, line) + line = lineStart + } + i++ + } + lines = append(lines, line) + return Grid{ + widgetBase: widgetBase{margin: 5}, + rowSpacing: 8, + colSpacing: 3, + rows: lines, } - } - if id == 0 { - preSelected = contactLabels[0] } - grid1 := Grid{ - widgetBase: widgetBase{name: "grid", margin: 5}, - rowSpacing: 8, - colSpacing: 3, - rows: append([][]GridE{ - { - {6, 1, Label{text: "Introduce a group of contacts to one another, revealing them to one another and exposing that you know every member of the group.", wrap: 200}}, - {1, 1, nil}, - {6, 1, Label{text: "Introduce one contact to group of contacts without revealing them to one another, and exposing that you the group to one contact.", wrap: 200}}, - }, { - {1, 1, nil}, - {1, 1, nil}, - {1, 1, Button{ - widgetBase: widgetBase{width: 40, name: "introduceAllWay"}, - text: "All-Way", - }}, - {1, 1, nil}, - {1, 1, nil}, - {1, 1, nil}, - {1, 1, nil}, - {1, 1, nil}, - {1, 1, Button{ - widgetBase: widgetBase{width: 40, name: "introduceOneMany"}, - text: "One-to-Many", - }}, - {3, 1, Combo{ - widgetBase: widgetBase{name: "selectedOneMany"}, - labels: contactLabels, - preSelected: preSelected, - }, - }, - {1, 1, nil}, + lhs := VBox{ + widgetBase: widgetBase{padding: 5}, + children: []Widget{ + Label{text: "Introducing some contacts to one another allows them to add each other as pond contacts. An introduction sends each a message from you containing information with which their pond client can automatically initiate key exchange.", wrap: 200}, + Label{text: ""}, + Label{text: "First, select contacts you wish to introduce to one another :", wrap: 200}, + ccbBuild(&toIntroduce), + Label{text: "Nest, select contacts you wish to introduce to the contacts selected above, but not to one another :", wrap: 200}, + ccbBuild(&toNormal), + Label{text: "Please keep in mind that introducing two contacts to one another reveals to both of them that you each of them.", wrap: 200}, + }, + } + rhs := VBox{ + widgetBase: widgetBase{padding: 5}, + children: []Widget{ + Button{ + widgetBase: widgetBase{name: "send", insensitive: true, padding: 5}, + text: "Introduce", }, - {{1, 1, nil}}, - { - {9, 1, Label{text: "Select a group of contacts :", wrap: 400}}, + Button{ + widgetBase: widgetBase{name: "draft", insensitive: false, padding: 5}, + text: "Draft", }, - }, contactsBoxes...), + }, } - grid1.rows = append(grid1.rows, messageCompose) + // We'll want a text box for entering a message eventually, but perhaps + // Ideally, we want two text boxes, one for the toIntroduce contacts and + // one for the toNormal contacts. + // + // text := Scrolled{ + // widgetBase: widgetBase{expand: true, fill: true}, + // horizontal: true, + // child: TextView{ + // widgetBase: widgetBase{expand: true, fill: true, name: "body"}, + // editable: true, + // wrap: true, + // updateOnChange: true, + // spellCheck: true, + // text: draft.body, + // }, + // } + c.gui.Actions() <- SetChild{name: "right", child: rightPane("INTRODUCE CONTACTS", lhs, rhs, nil)} + c.gui.Actions() <- UIState{uiStateIntroduceContact} + c.gui.Signal() - getId := func(name string) uint64 { - for i, n := range contactLabels { - if n == name { - return contactIds[i] - } + ccbClick := func(to *contactCheckBoxes, click Click) bool { + if !strings.HasPrefix(click.name, to.prefix) { + return false } - return 0 - } - sensitivity := func() { - c.gui.Actions() <- Sensitive{name: "introduceAllWay", sensitive: id != 0} - c.gui.Actions() <- Sensitive{name: "introduceOneMany", sensitive: id == 0} - c.gui.Actions() <- Sensitive{name: "selectedOneMany", sensitive: id != 0} - for i, id0 := range contactIds { - c.gui.Actions() <- Sensitive{name: fmt.Sprintf("%s%d", contactCheckBoxPrefix, i), sensitive: id0 != id} + t := click.name[len(to.prefix):] + id, err := strconv.ParseUint(t, 16, 64) + if _, ok := c.contacts[id]; err != nil || !ok { + panic(click.name) + } + checked := click.checks[click.name] + if checked { + addIdSet(to.set, id) + if noto := anti(to); noto != nil { + removeIdSet(noto.set, id) + c.gui.Actions() <- SetChecked{name: noto.prefix + t, checked: false} + } + } else { + removeIdSet(to.set, id) } + c.gui.Actions() <- Sensitive{name: "send", sensitive: len(*toIntroduce.set) > 0 && len(*toIntroduce.set)+len(*toNormal.set) > 1} c.gui.Signal() + return true } - //nextRow := len(grid.rows) - - c.gui.Actions() <- SetChild{name: "right", child: rightPane("INTRODUCE CONTACTS", grid1, nil, nil)} - c.gui.Actions() <- UIState{uiStateIntroduceContact} - c.gui.Signal() - - sensitivity() - for { event, wanted := c.nextEvent(0) if wanted { return event } - // if update, ok := event.(Update); ok { - // overSize = c.updateUsage(validContactSelected, draft) - // draft.body = update.text - // c.gui.Signal() - // continue - // } + // if update, ok := event.(Update); ok { + // updateUsage() + // draft.body = update.text + // c.gui.Signal() + // continue + // } click, ok := event.(Click) if !ok { continue } - switch { - case click.name == "introduceAllWay": - id = 0 - sensitivity() - continue - case click.name == "introduceOneMany": - id = getId(click.combos["selectedOneMany"]) - sensitivity() - continue - case click.name == "selectedOneMany": - id = getId(click.combos["selectedOneMany"]) - sensitivity() + + if ccbClick(&toIntroduce, click) { continue - case strings.HasPrefix(click.name, contactCheckBoxPrefix): - i, ok := strconv.Atoi(click.name[len(contactCheckBoxPrefix):]) - if ok != nil || i >= len(contactIds) { - continue - } - contactChecks[i] = click.checks[click.name] + } + if ccbClick(&toNormal, click) { continue - case click.name == "doIntroduce": - var cl contactList - if id != 0 { - cl = append(cl, c.contacts[id]) - } - for i = 0; i < len(contactIds); i++ { - if contactIds[i] == id { - continue - } - if contactChecks[i] { - cl = append(cl, c.contacts[contactIds[i]]) - } - } - - var urls []string - if id != 0 { - urls = c.introducePandaMessages_onemany(cl, true) - } else { - urls = c.introducePandaMessages_group(cl, true) - } - for i := range cl { - draft := c.newDraft(nil, []uint64{cl[i].id}, nil) - draft.body = messageBody + introducePandaMessageDesc + urls[i] - c.sendDraft(draft) - c.log.Printf("Queued introduction message for %s.", cl[i].name) - } - c.save() + } - c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} - c.gui.Actions() <- UIState{uiStateMain} - c.gui.Signal() - return nil + if click.name == "draft" { + // draft.body = click.textViews["body"] + return c.composeUI(draft) } + if click.name == "send" { + // draft.body = click.textViews["body"] + if r, err := c.composeUIsend(draft); err == nil { + return r + } + } } } @@ -3387,6 +3372,34 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { toBoxName := func(s string, i uint64) string { return fmt.Sprintf("to-box-%s-%x", s, i) } + var toBoxAddExplainId uint64 = 0 + toBoxAddExplain := func() { + var id uint64 + if len(draft.toIntroduce) > 0 { + id = draft.toIntroduce[0] + } else if len(draft.toNormal) > 0 { + id = draft.toNormal[0] + } else { + toBoxAddExplainId = 0 + return + } + toBoxAddExplainId = id + c.gui.Actions() <- Append{ + name: toBoxName("entry", id), + children: []Widget{ + Button{ + widgetBase: widgetBase{ + name: "explain-introductions", + font: "Liberation Sans 8", + foreground: colorBlue, + padding: 10, + width: 1, + }, + text: "i", + }, + }, + } + } toBoxAddEntry := func(id uint64, introduce bool) { if introduce { addIdSet(&draft.toIntroduce, id) @@ -3421,6 +3434,9 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { }, }, } + if len(draft.toIntroduce)+len(draft.toNormal) == 1 { + toBoxAddExplain() + } } originalToIntroduce := draft.toIntroduce draft.toIntroduce = nil @@ -3695,6 +3711,9 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { removeIdSet(&draft.toNormal, id) removeIdSet(&draft.toIntroduce, id) c.gui.Actions() <- Destroy{name: toBoxName("entry", id)} + if id == toBoxAddExplainId { + toBoxAddExplain() + } toBoxUpdateCombo() updateSend() introduceSensitivity() @@ -3791,52 +3810,66 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { c.gui.Signal() } - if click.name != "send" { - continue - } // if len(click.combos["to-box-add"]) > 0 { panic(click.name) } - draft.body = click.textViews["body"] - - messages, err := c.sendDraft(draft) - if err != nil { - // TODO: handle this case better. - println(err.Error()) - c.log.Errorf("Error sending message: %s", err) - continue - } - - for _, msg := range messages { - c.outboxUI.Add(msg.id, c.ContactName(msg.to), - msg.created.Format(shortTimeFormat), indicatorRed) + if click.name == "explain-introductions" { + draft.body = click.textViews["body"] + c.save() + return c.introduceUI(draft) } + if click.name == "send" { + if len(draft.toIntroduce)+len(draft.toNormal) == 0 { + panic("Can't send messge with no recipients") + } - if draft.inReplyTo != 0 { - for _, msg := range c.inbox { - if msg.id == draft.inReplyTo { - msg.acked = true - c.inboxUI.SetIndicator(msg.id, indicatorNone) - break - } + draft.body = click.textViews["body"] + if r, err := c.composeUIsend(draft); err == nil { + return r } } + } - c.draftsUI.Remove(draft.id) - delete(c.drafts, draft.id) - c.save() - if len(messages) == 1 { - id := messages[0].id - c.outboxUI.Select(id) - return c.showOutbox(id) - } else { - c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} - c.gui.Actions() <- UIState{uiStateMain} - c.gui.Signal() - return nil + return nil +} + +func (c *guiClient) composeUIsend(draft *Draft) (interface{}, error) { + messages, err := c.sendDraft(draft) + if err != nil { + // TODO: handle this case better. + println(err.Error()) + c.log.Errorf("Error sending message: %s", err) + return nil, err + } + + for _, msg := range messages { + c.outboxUI.Add(msg.id, c.ContactName(msg.to), + msg.created.Format(shortTimeFormat), indicatorRed) + } + + if draft.inReplyTo != 0 { + for _, msg := range c.inbox { + if msg.id == draft.inReplyTo { + msg.acked = true + c.inboxUI.SetIndicator(msg.id, indicatorNone) + break + } } } - return nil + c.draftsUI.Remove(draft.id) + delete(c.drafts, draft.id) + c.save() + + if len(messages) == 1 { + id := messages[0].id + c.outboxUI.Select(id) + return c.showOutbox(id), nil + } else { + c.gui.Actions() <- SetChild{name: "right", child: rightPlaceholderUI} + c.gui.Actions() <- UIState{uiStateMain} + c.gui.Signal() + return nil, nil + } } // unsealPendingMessages is run once a key exchange with a contact has From 343c0a48b49a91336ecf0564e66b9c5901beee61 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Wed, 26 Nov 2014 11:43:27 +0100 Subject: [PATCH 21/36] Save toNormal and toIntroduce to state file --- client/disk.go | 16 +++++++++------- client/disk/client.proto | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/client/disk.go b/client/disk.go index 4519aba..177c4f5 100644 --- a/client/disk.go +++ b/client/disk.go @@ -250,10 +250,14 @@ func (c *client) unmarshal(state *disk.State) error { created: time.Unix(*m.Created, 0), } c.registerId(draft.id) - if m.To != nil { - // draft.to - draft.toNormal = []uint64{*m.To} + + if m.ToNormal != nil && len(m.ToNormal) > 0 { + draft.toNormal = m.ToNormal + } + if m.ToIntroduce != nil && len(m.ToIntroduce) > 0 { + draft.toIntroduce = m.ToIntroduce } + if m.InReplyTo != nil { draft.inReplyTo = *m.InReplyTo } @@ -384,10 +388,8 @@ func (c *client) marshal() []byte { Attachments: draft.attachments, Detachments: draft.detachments, Created: proto.Int64(draft.created.Unix()), - } - // draft.to - if len(draft.toNormal) > 0 { - m.To = proto.Uint64(draft.toNormal[0]) + ToNormal: draft.toNormal, + ToIntroduce: draft.toIntroduce, } if draft.inReplyTo != 0 { diff --git a/client/disk/client.proto b/client/disk/client.proto index 7ec7d15..4778e91 100644 --- a/client/disk/client.proto +++ b/client/disk/client.proto @@ -134,7 +134,8 @@ message Outbox { message Draft { required fixed64 id = 1; required int64 created = 2; - optional fixed64 to = 3; + repeated fixed64 to_normal = 3; + repeated fixed64 to_introduce = 8; required string body = 4; optional fixed64 in_reply_to = 5; repeated protos.Message.Attachment attachments = 6; From 4a2bad1f927b6e47d2bcf4569a9b5a19c72fa5ae Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Wed, 26 Nov 2014 15:35:33 +0100 Subject: [PATCH 22/36] Rebuild disk/client.proto with toNormal and toIntroduce changes --- client/disk/client.pb.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/client/disk/client.pb.go b/client/disk/client.pb.go index 8738592..5138fc3 100644 --- a/client/disk/client.pb.go +++ b/client/disk/client.pb.go @@ -756,7 +756,8 @@ func (m *Outbox) GetRevocation() bool { type Draft struct { Id *uint64 `protobuf:"fixed64,1,req,name=id" json:"id,omitempty"` Created *int64 `protobuf:"varint,2,req,name=created" json:"created,omitempty"` - To *uint64 `protobuf:"fixed64,3,opt,name=to" json:"to,omitempty"` + ToNormal []uint64 `protobuf:"fixed64,3,rep,name=to_normal" json:"to_normal,omitempty"` + ToIntroduce []uint64 `protobuf:"fixed64,8,rep,name=to_introduce" json:"to_introduce,omitempty"` Body *string `protobuf:"bytes,4,req,name=body" json:"body,omitempty"` InReplyTo *uint64 `protobuf:"fixed64,5,opt,name=in_reply_to" json:"in_reply_to,omitempty"` Attachments []*protos.Message_Attachment `protobuf:"bytes,6,rep,name=attachments" json:"attachments,omitempty"` @@ -782,11 +783,18 @@ func (m *Draft) GetCreated() int64 { return 0 } -func (m *Draft) GetTo() uint64 { - if m != nil && m.To != nil { - return *m.To +func (m *Draft) GetToNormal() []uint64 { + if m != nil { + return m.ToNormal } - return 0 + return nil +} + +func (m *Draft) GetToIntroduce() []uint64 { + if m != nil { + return m.ToIntroduce + } + return nil } func (m *Draft) GetBody() string { From a27f0fc498d403c35fb025e68742c9118260afba Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Wed, 26 Nov 2014 17:28:41 +0100 Subject: [PATCH 23/36] Allow users to disable collecting the social graph data We allow users to disable collecting the social graph data for a specific contact. If *all* contacts have collecting social graph data disabled then any new contacts begin with collecting social graph data disabled as well. We now avoid calling fixProposedContactName in parsePandaURLsText on existing contacts too. --- client/cli.go | 1 + client/client.go | 9 +------ client/gui.go | 34 ++++++++++++++++++++++--- client/introduce.go | 61 ++++++++++++++++++++++++++++++++++++--------- 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/client/cli.go b/client/cli.go index 3221b6d..f599ff6 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1592,6 +1592,7 @@ Handle: id: c.randId(), cliId: c.newCliId(), } + c.initSocialGraphRecords(contact) stack := &panda.CardStack{ NumDecks: 1, diff --git a/client/client.go b/client/client.go index b5f2eb2..309948b 100644 --- a/client/client.go +++ b/client/client.go @@ -1233,14 +1233,7 @@ func (c *client) deleteContact(contact *Contact) { removeIdSet(&draft.toNormal, contact.id) removeIdSet(&draft.toIntroduce, contact.id) } - - for id, contact := range c.contacts { - if contact.introducedBy == id { - contact.introducedBy = 0 - } - removeIdSet(&contact.verifiedBy, id) - removeIdSet(&contact.introducedTo, id) - } + c.deleteSocialGraphRecords(contact.id) c.queueMutex.Lock() var newQueue []*queuedMessage diff --git a/client/gui.go b/client/gui.go index 5c21655..87a084b 100644 --- a/client/gui.go +++ b/client/gui.go @@ -2147,6 +2147,7 @@ func (c *guiClient) showContact(id uint64) interface{} { entries = append(entries, nvEntry{"GROUP GENERATION", fmt.Sprintf("%d", contact.generation)}, nvEntry{"CLIENT VERSION", fmt.Sprintf("%d", contact.supportedVersion)}) + rowName := 0 var pandaMessage string @@ -2164,7 +2165,10 @@ func (c *guiClient) showContact(id uint64) interface{} { entries = append(entries, nvEntry{"KEY EXCHANGE", string(out.Bytes())}) } - if contact.introducedBy != 0 { + rowDarkWebOfTrust := len(entries) + entries = append(entries, nvEntry{"", ""}) + + if contact.introducedBy != 0 && contact.introducedBy != disableDarkWebOfTrust { cnt, ok := c.contacts[contact.introducedBy] name := "Unknown" if ok { @@ -2228,7 +2232,7 @@ func (c *guiClient) showContact(id uint64) interface{} { left := nameValuesLHS(entries) // Switch the label next to "name" with an entry and a button. - left.rows[0][1].widget = HBox{ + left.rows[rowName][1].widget = HBox{ children: []Widget{ Entry{ widgetBase: widgetBase{ @@ -2247,6 +2251,19 @@ func (c *guiClient) showContact(id uint64) interface{} { }, } + left.rows[rowDarkWebOfTrust][1].widget = HBox{ + children: []Widget{ + CheckButton{ + widgetBase: widgetBase{ + name: "disableSocialGraph", + padding: 2, + }, + checked: contact.keepSocialGraphRecords(), + text: "Retain introduction records", + }, + }, + } + c.gui.Actions() <- SetChild{name: "right", child: rightPane("CONTACT", left, right, nil)} c.gui.Actions() <- UIState{uiStateShowContact} c.gui.Signal() @@ -2315,8 +2332,18 @@ func (c *guiClient) showContact(id uint64) interface{} { c.contactsUI.SetLine(contact.id, newName) c.gui.Actions() <- UIState{uiStateContactNameChanged} c.gui.Signal() - c.save() + + case "disableSocialGraph": + if !click.checks["disableSocialGraph"] { + c.deleteSocialGraphRecords(id) + contact.introducedBy = disableDarkWebOfTrust + contact.verifiedBy = nil + contact.introducedTo = nil + } else if contact.introducedBy == disableDarkWebOfTrust { + contact.introducedBy = 0 + } // else { panic("unreachable") } + case "composeTo": return c.composeUI(c.newDraftUI([]uint64{contact.id}, nil, nil)) case "introduceTo": @@ -2651,6 +2678,7 @@ Manual keying (not generally recommended) involves exchanging key material with isPending: true, id: c.randId(), } + c.initSocialGraphRecords(contact) c.gui.Actions() <- SetText{name: "error1", text: ""} c.gui.Actions() <- Sensitive{name: "name", sensitive: false} diff --git a/client/introduce.go b/client/introduce.go index 42a8fb9..ef8bc9b 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -9,6 +9,7 @@ import ( ) const ( + disableDarkWebOfTrust = 0xFFFFFFFFFFFFFFFF introducePandaMessageDesc = "\n---- Introduction URIs for proposed new contacts ----\n" ) @@ -46,6 +47,38 @@ func (c *client) contactListFromIdSet(set []uint64) (ci contactList) { return } +func (contact *Contact) keepSocialGraphRecords() bool { + return contact.introducedBy != disableDarkWebOfTrust +} + +func (c *client) initSocialGraphRecords(contact *Contact) { + // If all existing contacts have the Dark Web of Trust disabled then + // new contacts should start with the Dark Web of Trust disabled too. + if contact.introducedBy != 0 { + return + } + if c.contacts == nil || len(c.contacts) == 0 { + return + } + contact.introducedBy = disableDarkWebOfTrust + for _, cnt := range c.contacts { + if cnt.introducedBy != disableDarkWebOfTrust { + contact.introducedBy = 0 + break + } + } +} + +func (c *client) deleteSocialGraphRecords(id uint64) { + for _, contact := range c.contacts { + if contact.introducedBy == id { + contact.introducedBy = 0 + } + removeIdSet(&contact.verifiedBy, id) + removeIdSet(&contact.introducedTo, id) + } +} + func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (string, string) { panda_secret := panda.NewSecretString(c.rand)[2:] s := func(cnt *Contact) string { @@ -53,7 +86,7 @@ func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (st url.QueryEscape(cnt.name), panda_secret, cnt.theirIdentityPublic) // no EncodeToString? } - if real { + if real && cnt1.keepSocialGraphRecords() && cnt2.keepSocialGraphRecords() { addIdSet(&cnt1.introducedTo, cnt2.id) addIdSet(&cnt2.introducedTo, cnt1.id) } @@ -182,10 +215,11 @@ func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContac c.log.Printf("Bad public identity %s, skipping.", m[urlparse_theirIdentityPublic]) continue } - if contact, found := c.contactByIdentity(pc.theirIdentityPublic[:]); found { - pc.id = contact.id - if contact.introducedBy != sender { - addIdSet(&contact.verifiedBy, sender) + existing, found := c.contactByIdentity(pc.theirIdentityPublic[:]) + if found && c.contacts[sender].keepSocialGraphRecords() { + pc.id = existing.id + if existing.introducedBy != sender && existing.keepSocialGraphRecords() { + addIdSet(&existing.verifiedBy, sender) } } @@ -195,7 +229,9 @@ func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContac } else { pc.name = n } - c.fixProposedContactName(pc, sender) + if !found { + c.fixProposedContactName(pc, sender) + } l = append(l, pc) } @@ -229,13 +265,14 @@ func (c *client) beginProposedPandaKeyExchange(pc ProposedContact, introducedBy isPending: true, id: c.randId(), theirIdentityPublic: pc.theirIdentityPublic, - introducedBy: introducedBy, } - // theirIdentityPublic is set only for contacts pending by introduction - // if introducedBy != 0 { - // contact.introducedBy = introducedBy - // copy(contact.theirIdentityPublic[:], pc.theirIdentityPublic[:]) - // } + // theirIdentityPublic is only set only for contacts pending by introduction + if c.contacts[introducedBy].keepSocialGraphRecords() { + contact.introducedBy = introducedBy + } else { + c.log.Printf("Introduced contact %s is not marked as introduced by %s because %s has keeping such records disabled.\n", + pc.name, c.contacts[introducedBy].name, c.contacts[introducedBy].name) + } stack := &panda.CardStack{ NumDecks: 1, From dc75614ebc90ae8cb37d997fabba7312346ab206 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sat, 13 Dec 2014 20:55:42 -0500 Subject: [PATCH 24/36] Fixed tests to use new composeUI interface In particular, we never create the to-box-add combobox if no valid contacts exist now, while our old to combobox was created empty, so TestSendToPendingContact cannot check the ok value anymore. --- client/client_test.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 07acac7..1ace456 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -669,9 +669,13 @@ func composeMessage(client *TestClient, to string, message string) { client.gui.events <- Click{name: "compose"} client.AdvanceTo(uiStateCompose) + client.gui.events <- Click{ + name: "to-box-add", + combos: map[string]string{"to-box-add": to}, + } + client.gui.events <- Click{ name: "send", - combos: map[string]string{"to": to}, textViews: map[string]string{"body": message}, } @@ -1201,9 +1205,13 @@ func testDetached(t *testing.T, upload bool) { t.Errorf("detachments still empty") } + client1.gui.events <- Click{ + name: "to-box-add", + combos: map[string]string{"to-box-add": "client2"}, + } + client1.gui.events <- Click{ name: "send", - combos: map[string]string{"to": "client2"}, textViews: map[string]string{"body": "foo"}, } @@ -1723,9 +1731,13 @@ func testReplyACKs(t *testing.T, reloadDraft bool, abortSend bool) { client2.AdvanceTo(uiStateCompose) } + client2.gui.events <- Click{ + name: "to-box-add", + combos: map[string]string{"to-box-add": "client1"}, + } + client2.gui.events <- Click{ name: "send", - combos: map[string]string{"to": "client1"}, textViews: map[string]string{"body": "reply message"}, } client2.AdvanceTo(uiStateOutbox) @@ -1734,11 +1746,16 @@ func testReplyACKs(t *testing.T, reloadDraft bool, abortSend bool) { client2.gui.events <- Click{name: "abort"} client2.AdvanceTo(uiStateCompose) + client2.gui.events <- Click{ + name: "to-box-add", + combos: map[string]string{"to-box-add": "client1"}, + } + client2.gui.events <- Click{ name: "send", - combos: map[string]string{"to": "client1"}, textViews: map[string]string{"body": "reply message"}, } + client2.AdvanceTo(uiStateOutbox) } @@ -1822,7 +1839,7 @@ func TestSendToPendingContact(t *testing.T) { client.gui.events <- Click{name: "compose"} client.AdvanceTo(uiStateCompose) - if contacts, ok := client.gui.combos["to"]; !ok || len(contacts) > 0 { + if contacts, _ := client.gui.combos["to-box-add"]; len(contacts) > 0 { t.Error("can send message to pending contact") } } From d3cef9e52a1453011ae8292265610cea66411fb9 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sun, 14 Dec 2014 17:04:33 -0500 Subject: [PATCH 25/36] Make usageString() report size more accurately We removed the rough estimation in Draft.usageString(), replacing it with client.usageString(Draft). Ideally, we should avoid marshalling the entire message including attachments with every keypress, but hey. --- client/cli.go | 2 +- client/client.go | 19 ++++++++++++++----- client/gui.go | 14 +++++++++----- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/client/cli.go b/client/cli.go index f599ff6..76c7145 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1041,7 +1041,7 @@ func (c *cliClient) showQueueState() { } func (c *cliClient) printDraftSize(draft *Draft) { - usageString, oversize := draft.usageString() + usageString, oversize := c.usageString(draft) prefix := termPrefix if oversize { prefix = termErrPrefix diff --git a/client/client.go b/client/client.go index 309948b..9872964 100644 --- a/client/client.go +++ b/client/client.go @@ -608,7 +608,7 @@ func prettyNumber(n uint64) string { // usageString returns a description of the amount of space taken up by a body // with the given contents and a bool indicating overflow. -func (draft *Draft) usageString() (string, bool) { +func (c *client) usageString(draft *Draft) (string, bool) { var replyToId *uint64 if draft.inReplyTo != 0 { replyToId = proto.Uint64(1) @@ -633,10 +633,19 @@ func (draft *Draft) usageString() (string, bool) { } l := uint64(len(serialized)) - // We must overestimate the size of introductions - to := append(draft.toNormal, draft.toIntroduce...) - expectedContactNameSize := 30 - l += uint64((len(to) - 1) * (len("pond-introduce-panda://") + 64 + expectedContactNameSize)) + // We estimate the size by the larges introduction message size + if len(draft.toIntroduce) > 0 && len(draft.toIntroduce)+len(draft.toNormal) > 1 { + urlsIntroduce, urlsNormal := c.introducePandaMessages( + c.contactListFromIdSet(draft.toIntroduce), + c.contactListFromIdSet(draft.toNormal), false) + var m int = 0 + for _, s := range append(urlsIntroduce, urlsNormal...) { + if len(s) > m { + m = len(s) + } + } + l += uint64(len(introducePandaMessageDesc) + m) + } s := fmt.Sprintf("%s of %s bytes", prettyNumber(l), prettyNumber(pond.MaxSerializedMessage)) return s, len(serialized) > pond.MaxSerializedMessage diff --git a/client/gui.go b/client/gui.go index 87a084b..0387951 100644 --- a/client/gui.go +++ b/client/gui.go @@ -3276,7 +3276,7 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { // We modify overSize in updateSend() and updateUsage() // We modify usageMessage in updateUsage() but do not use it currently - usageMessage, overSize := draft.usageString() + usageMessage, overSize := c.usageString(draft) lhs := VBox{ children: []Widget{ @@ -3580,7 +3580,8 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { } updateUsage := func() { - usageMessage, overSize = draft.usageString() + // We should avoid marshaling the attachments with every keypress here + usageMessage, overSize = c.usageString(draft) c.gui.Actions() <- SetText{name: "usage", text: usageMessage} color := uint32(colorBlack) if overSize { @@ -3724,7 +3725,8 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { introduce := len(draft.toIntroduce) > 0 && len(draft.toNormal) == 0 toBoxAddEntry(contact.id, introduce) toBoxUpdateCombo() - updateSend() + updateUsage() + // updateSend() introduceSensitivity() c.draftsUI.SetLine(draft.id, c.sideDraftRecipients(draft)) c.gui.Signal() @@ -3743,7 +3745,8 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { toBoxAddExplain() } toBoxUpdateCombo() - updateSend() + updateUsage() + // updateSend() introduceSensitivity() c.draftsUI.SetLine(draft.id, c.sideDraftRecipients(draft)) c.gui.Signal() @@ -3763,7 +3766,8 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { addIdSet(&draft.toNormal, id) removeIdSet(&draft.toIntroduce, id) } - // c.gui.Signal() + updateUsage() + c.gui.Signal() continue } From 824accec9d8d20974e1ae852cabeab670b430ed6 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 15 Dec 2014 02:23:04 -0500 Subject: [PATCH 26/36] Initial TestIntroductions Rough test for introductions and necessarily multirecipient message, maybe should test more, like message contents. --- client/client_test.go | 136 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 121 insertions(+), 15 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 1ace456..2a9b659 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1634,26 +1634,22 @@ func TestPANDA(t *testing.T) { }() wg.Wait() - var client2FromClient1 *Contact - for _, contact := range client1.contacts { - client2FromClient1 = contact - break - } + verifyGenerationSymetric(t, client1, client2, "client1", "client2") +} - var client1FromClient2 *Contact - for _, contact := range client2.contacts { - client1FromClient2 = contact - break +func verifyGeneration(t *testing.T, client1, client2 *TestClient, client2petname string) { + client2FromClient1, ok := client1.contactByName(client2petname) + if !ok { + panic("name not found") } - if g := client2FromClient1.generation; g != client2.generation { - t.Errorf("Generation mismatch %d vs %d", g, client1.generation) - } - - if g := client1FromClient2.generation; g != client1.generation { - t.Errorf("Generation mismatch %d vs %d", g, client1.generation) + t.Errorf("Generation mismatch %d vs %d", g, client2.generation) } } +func verifyGenerationSymetric(t *testing.T, client1, client2 *TestClient, client1petname, client2petname string) { + verifyGeneration(t, client1, client2, client2petname) + verifyGeneration(t, client2, client1, client1petname) +} func TestReadingOldStateFiles(t *testing.T) { if parallel { @@ -2446,3 +2442,113 @@ func TestContactNameChange(t *testing.T) { t.Errorf("name not updated in client after reload") } } + +func toBoxName(s string, i uint64) string { + return fmt.Sprintf("to-box-%s-%x", s, i) +} +func composeMessageStart(client *TestClient) { + client.gui.events <- Click{name: "compose"} + client.AdvanceTo(uiStateCompose) +} +func composeMessageAdd(client *TestClient, toBoth ...string) { + for _, to := range toBoth { + client.gui.events <- Click{ + name: "to-box-add", + combos: map[string]string{"to-box-add": to}, + } + } +} +func composeMessageIntroduce(client *TestClient, toIntroduce ...string) { + for _, to := range toIntroduce { + contact, ok := client.contactByName(to) + if !ok { + panic("name not found") + } + n := toBoxName("introduce", contact.id) + client.gui.events <- Click{ + name: n, + checks: map[string]bool{n: true}, + } + } +} +func composeMessageSendMany(client *TestClient, message string) { + client.gui.events <- Click{ + name: "send", + textViews: map[string]string{"body": message}, + } + // Should be uiStateOutbox once the outbox supports multiple recipients + client.AdvanceTo(uiStateMain) +} + +func TestIntroductions(t *testing.T) { + if parallel { + t.Parallel() + } + + server, err := NewTestServer(t) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + clientName := func(i int) string { return fmt.Sprintf("client%d", i) } + + clients := []*TestClient{} + for i := 0; i < 4; i++ { + client, err := NewTestClient(t, clientName(i), nil) + if err != nil { + t.Fatal(err) + } + clients = append(clients, client) + } + defer func() { + for i := 0; i < 4; i++ { + clients[i].Close() + } + }() + + for i := 1; i < 4; i++ { + proceedToPairedWithNames(t, clients[0], clients[i], + "client0", clientName(i), server) + } + + composeMessageStart(clients[0]) + composeMessageAdd(clients[0], "client1", "client2", "client3") + composeMessageIntroduce(clients[0], "client1") + composeMessageSendMany(clients[0], "test message") + + for i := 1; i < 4; i++ { + transmitMessage(clients[0], false) + } + + for _, client := range clients[1:] { + from, _ := fetchMessage(client) + if from != "client0" { + t.Fatalf("message from %s, expected client0", from) + } + } + + var wg sync.WaitGroup + wg.Add(4) + doGreet := func(client *TestClient, greet uint) { + client.gui.events <- Click{ + name: client.inboxUI.entries[0].boxName, + } + client.AdvanceTo(uiStateInbox) + client.gui.events <- Click{ + name: fmt.Sprintf("greet-%d", greet), + } + go func() { + client.AdvanceTo(uiStatePANDAComplete) + wg.Done() + }() + } + doGreet(clients[1], 0) + doGreet(clients[1], 1) + doGreet(clients[2], 0) + doGreet(clients[3], 0) + wg.Wait() + + verifyGenerationSymetric(t, clients[1], clients[2], "client1", "client2") + verifyGenerationSymetric(t, clients[1], clients[3], "client1", "client3") +} From 45116812d95835589521c7f6efe458b2211b2a92 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Mon, 22 Dec 2014 01:21:49 -0500 Subject: [PATCH 27/36] Use logEvent during susupcious greets We fix a bug where fixProposedContactName didn't actually rename the ProposedContact too. Ideally we should use a fuzzy test for name similarity, maybe based on n-grams or maybe a fast fuzzy spelling suggestion algorithm : https://github.com/sajari/fuzzy or even JaroWinkler or Levenshtein from "github.com/antzucaro/matchr". --- client/introduce.go | 48 ++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/client/introduce.go b/client/introduce.go index ef8bc9b..eacd474 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -152,18 +152,18 @@ type ProposedContact struct { theirIdentityPublic [32]byte name string id uint64 // zero if new or failed + onGreet func(*Contact) } -func (c *client) fixProposedContactName(pc ProposedContact, sender uint64) { - // We should consider using JaroWinkler or Levenshtein from +func (c *client) fixProposedContactName(pc *ProposedContact, sender uint64) { + // We should a fuzzy test for name similarity, maybe based on n-grams + // or maybe a fast fuzzy spelling suggestion algorithm + // https://github.com/sajari/fuzzy + // or even JaroWinkler or Levenshtein from // "github.com/antzucaro/matchr" here : // https://godoc.org/github.com/antzucaro/matchr#JaroWinkler - // Or maybe a fast fuzzy spelling suggestion algorithm - // https://github.com/sajari/fuzzy - // for _, contact := range c.contacts { } - // At least we now alphabatize the contacts listing however. s := "" - _, ok := c.contactByName(pc.name) + conflict0, ok := c.contactByName(pc.name) if !ok { return } @@ -180,12 +180,29 @@ func (c *client) fixProposedContactName(pc ProposedContact, sender uint64) { pc.name, s, c.contacts[sender].name) pc.name += s - // if userlog { - // e := fmt.Sprintf("%s suggested the name %s for %s. Verify that nothing nefarious happened and rename them if desired.", - // c.contacts[sender].name,pc.name,c.contacts[id1].name); - // c.logEvent(c.contacts[id1],e) - // c.logEvent(c.contacts[sender],e) - // } // We need to be able to logEvent to the proposed contact here too. + e := fmt.Sprintf("%s suggested the name %s for %s.", + c.contacts[sender].name, conflict0.name, pc.name) + if i := conflict0.introducedBy; i != 0 && i != disableDarkWebOfTrust { + e += fmt.Sprintf(" Also %s was previously introduced by %s. Do you trust both %s and %s?", + conflict0.name, c.contacts[i].name, + c.contacts[sender].name, c.contacts[i].name) + } else { + e += fmt.Sprintf(" Do you trust %s?", c.contacts[i].name) + } + + id0 := conflict0.id + pc.onGreet = func(cnt1 *Contact) { + c.logEvent(cnt1, e) + logEvent := func(id uint64) { + if cnt, ok := c.contacts[id]; ok { + c.logEvent(cnt, e) + } else { + c.log.Printf("Failed logEvent : Contact involved in introduction was deleted?") + } + } + logEvent(sender) + logEvent(id0) + } } // Finds and parses all the pond-introduce-panda URLs in a message body. @@ -230,7 +247,7 @@ func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContac pc.name = n } if !found { - c.fixProposedContactName(pc, sender) + c.fixProposedContactName(&pc, sender) } l = append(l, pc) @@ -273,6 +290,9 @@ func (c *client) beginProposedPandaKeyExchange(pc ProposedContact, introducedBy c.log.Printf("Introduced contact %s is not marked as introduced by %s because %s has keeping such records disabled.\n", pc.name, c.contacts[introducedBy].name, c.contacts[introducedBy].name) } + if pc.onGreet != nil { + pc.onGreet(contact) + } stack := &panda.CardStack{ NumDecks: 1, From e22b5c1239ea6fc9c278abda2f1eb1baed1fa258 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 26 Dec 2014 14:45:58 -0500 Subject: [PATCH 28/36] Use opaque URI format Repalce the pond-introduce-panda://NAME/SECRET/IDENTITY/ URI format with the opaque URI format pond-introduce:NAME?pandaSecret=SECRET&identity=IDENTITY --- client/introduce.go | 79 +++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/client/introduce.go b/client/introduce.go index eacd474..963c14a 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -82,9 +82,16 @@ func (c *client) deleteSocialGraphRecords(id uint64) { func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (string, string) { panda_secret := panda.NewSecretString(c.rand)[2:] s := func(cnt *Contact) string { - return fmt.Sprintf("pond-introduce-panda://%s/%s/%x/\n", - url.QueryEscape(cnt.name), panda_secret, - cnt.theirIdentityPublic) // no EncodeToString? + v := url.Values{ + "pandaSecret": {panda_secret}, + "identity": {fmt.Sprintf("%x", cnt.theirIdentityPublic)}, + } + u := url.URL{ + Scheme: "pond-introduce", + Opaque: url.QueryEscape(cnt.name), + RawQuery: v.Encode(), + } + return u.String() + "#" } if real && cnt1.keepSocialGraphRecords() && cnt2.keepSocialGraphRecords() { addIdSet(&cnt1.introducedTo, cnt2.id) @@ -205,31 +212,57 @@ func (c *client) fixProposedContactName(pc *ProposedContact, sender uint64) { } } -// Finds and parses all the pond-introduce-panda URLs in a message body. +func parseKnownOpaqueURI(s string) (opaque string, vs url.Values, err error) { + u, e := url.Parse(s) + opaque = u.Opaque + if e != nil { + err = e + } else { + vs, err = url.ParseQuery(u.RawQuery) + } + return +} + +func singletonValues(values url.Values) bool { + for _, l := range values { + if len(l) > 1 { + return false + } + } + return true +} + +// Finds and parses all the pond-introduce URIs in a message body. // Returns a list of ProposedContacts from which to create add contact buttons. // We allow contacts to be added even if they fail most checks here because // maybe they're the legit contact and the existing one is bad. func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContact { var l []ProposedContact - re := regexp.MustCompile("(pond-introduce-panda)://([^/]+)/([^/]+)/([0-9A-Fa-f]{64})/") - ms := re.FindAllStringSubmatch(body, -1) // -1 means find all - const ( - urlparse_protocol = 1 - urlparse_name = 2 - urlparse_sharedSecret = 3 - urlparse_theirIdentityPublic = 4 - ) + re := regexp.MustCompile("pond-introduce:([^& ?#]+)\\?([^& ?#]+)(&([^& ?#]+))*") + ms := re.FindAllString(body, -1) // -1 means find all for _, m := range ms { - if !panda.IsAcceptableSecretString(m[urlparse_sharedSecret]) { - c.log.Printf("Unacceptably weak secret '%s' for %s.", - m[urlparse_sharedSecret], m[urlparse_name]) + opaque, vs, err := parseKnownOpaqueURI(m) + if err != nil || !singletonValues(vs) { + c.log.Printf("Malformed pond-introduce: URI : %s", m) + continue } var pc ProposedContact - pc.sharedSecret = m[urlparse_sharedSecret] + pc.name, err = url.QueryUnescape(opaque) + if err != nil { + c.log.Printf("Malformed pond-introduce: URI : %s", m) + continue + } - if !hexDecodeSafe(pc.theirIdentityPublic[:], m[urlparse_theirIdentityPublic]) { - c.log.Printf("Bad public identity %s, skipping.", m[urlparse_theirIdentityPublic]) + pc.sharedSecret = vs.Get("pandaSecret") + if !panda.IsAcceptableSecretString(pc.sharedSecret) { + c.log.Printf("Unacceptably weak secret '%s' for %s, continuing.", + pc.sharedSecret, pc.name) + } + + identity := vs.Get("identity") + if !hexDecodeSafe(pc.theirIdentityPublic[:], identity) || len(identity) != 64 { + c.log.Printf("Bad public identity %s, skipping.", identity) continue } existing, found := c.contactByIdentity(pc.theirIdentityPublic[:]) @@ -239,13 +272,11 @@ func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContac addIdSet(&existing.verifiedBy, sender) } } - - n, err := url.QueryUnescape(m[urlparse_name]) - if err != nil { - c.log.Printf("Badly escaped name %s, fix using rename.", m[urlparse_name]) - } else { - pc.name = n + if pc.name == "" { + c.log.Printf("Empty contact name, using identity %s.", identity) + pc.name = identity } + if !found { c.fixProposedContactName(&pc, sender) } From 7b5927f2d17db7dd9c976c9879ca24160e1995ae Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 26 Dec 2014 15:37:19 -0500 Subject: [PATCH 29/36] Use NewSimpleMeetingPlace in TestIntroductions We need NewSimpleMeetingPlace to avoid hitting the PANDA server when testing, but an intermitent race condition is created here. I disabled the two calls to clients[1].ReloadWithMeetingPlace(mp) in TestIntroductions because they break the client.gui.events channel preventing the uiStatePANDAComplete message from arriving. Appears ReloadWithMeetingPlace helps TestPANDA avoid a data race, but the issue haasn't arisen for me. If we ReloadWithMeetingPlace then doGreet should be split into two seperate functions to actually press greet and create the goroutine to wait for it. I suspect however that launching the listening goroutine before doing the greet on the other side avoids this data race better. --- client/client_test.go | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 2a9b659..56659d3 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2491,7 +2491,12 @@ func TestIntroductions(t *testing.T) { } defer server.Close() - clientName := func(i int) string { return fmt.Sprintf("client%d", i) } + clientName := func(i int) string { return fmt.Sprintf("client %d", i) } + + mp := panda.NewSimpleMeetingPlace() + newMeetingPlace := func() panda.MeetingPlace { + return mp + } clients := []*TestClient{} for i := 0; i < 4; i++ { @@ -2499,6 +2504,7 @@ func TestIntroductions(t *testing.T) { if err != nil { t.Fatal(err) } + client.newMeetingPlace = newMeetingPlace clients = append(clients, client) } defer func() { @@ -2509,12 +2515,12 @@ func TestIntroductions(t *testing.T) { for i := 1; i < 4; i++ { proceedToPairedWithNames(t, clients[0], clients[i], - "client0", clientName(i), server) + "client 0", clientName(i), server) } composeMessageStart(clients[0]) - composeMessageAdd(clients[0], "client1", "client2", "client3") - composeMessageIntroduce(clients[0], "client1") + composeMessageAdd(clients[0], "client 1", "client 2", "client 3") + composeMessageIntroduce(clients[0], "client 1") composeMessageSendMany(clients[0], "test message") for i := 1; i < 4; i++ { @@ -2523,13 +2529,12 @@ func TestIntroductions(t *testing.T) { for _, client := range clients[1:] { from, _ := fetchMessage(client) - if from != "client0" { - t.Fatalf("message from %s, expected client0", from) + if from != "client 0" { + t.Fatalf("message from %s, expected client 0", from) } } var wg sync.WaitGroup - wg.Add(4) doGreet := func(client *TestClient, greet uint) { client.gui.events <- Click{ name: client.inboxUI.entries[0].boxName, @@ -2543,12 +2548,22 @@ func TestIntroductions(t *testing.T) { wg.Done() }() } + + wg.Add(2) doGreet(clients[1], 0) - doGreet(clients[1], 1) + // clients[1].ReloadWithMeetingPlace(mp) doGreet(clients[2], 0) + wg.Wait() + + verifyGenerationSymetric(t, clients[1], clients[2], "client 1", "client 2") + + wg.Add(2) + doGreet(clients[1], 1) + // clients[1].ReloadWithMeetingPlace(mp) doGreet(clients[3], 0) wg.Wait() - verifyGenerationSymetric(t, clients[1], clients[2], "client1", "client2") - verifyGenerationSymetric(t, clients[1], clients[3], "client1", "client3") + verifyGenerationSymetric(t, clients[1], clients[3], "client 1", "client 3") + + // verifyUnpaired(clients[2],clients[3]) } From c606502580aecc4d3241f62a854892cb7ca217d7 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 23 Jan 2015 16:52:11 +0100 Subject: [PATCH 30/36] Do introductions in pond.Message protobuf We handled introduction in the message protobuf itself, creating with type pond.Message_Introduction. We retain the ability to build greets based upon introductions that arrive as opaque URIs because that might prove useful for compatibility with older clients. --- client/cli.go | 57 +++++-------- client/disk/client.proto | 1 + client/gui.go | 12 ++- client/introduce.go | 179 +++++++++++++++++++++++---------------- client/network.go | 20 +++-- protos/pond.proto | 10 +++ 6 files changed, 156 insertions(+), 123 deletions(-) diff --git a/client/cli.go b/client/cli.go index 76c7145..13d47fa 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1625,38 +1625,23 @@ Handle: if len(cl) == 0 { return } - cl = append(contactList{contact}, cl...) // Build from notes eventually - prebody0 := "To: " + cl[1].name - for _, to := range cl[2:] { - prebody0 += ", " + to.name - } - prebody0 += "\n\n" - body0, ok := c.inputTextBlock(prebody0, true) - if !ok { - c.Printf("Not OK, what now?") + prebody := "To: " + contact.name + for _, to := range cl { + prebody += ", " + to.name } - bodyn, ok := c.inputTextBlock("To: "+cl[0].name+"\n\n", true) + prebody += "\n\n" + body, ok := c.inputTextBlock(prebody, true) if !ok { c.Printf("Not OK, what now?") } - // c.introduceContact_onemany(contact,cl) - urls := c.introducePandaMessages_onemany(cl, true) - for i := range cl { - draft := c.newDraft([]uint64{cl[i].id}, nil, nil) - draft.cliId = c.newCliId() - if i == 0 { - draft.body = body0 - } else { - draft.body = bodyn - } - draft.body += introducePandaMessageDesc + urls[i] - c.sendDraft(draft) - c.Printf("%s Sending introduction message %s%s%s to %s\n", termInfoPrefix, - termCliIdStart, draft.cliId.String(), termReset, cl[i].name) - } + draft := c.newDraft([]uint64{contact.id}, contactListToIdSet(cl), nil) + draft.body = body + c.sendDraft(draft) + c.Printf("%s Sending introduction message %s%s%s for %s to %d other contacts.\n", termInfoPrefix, + termCliIdStart, draft.cliId.String(), termReset, contact.name, len(cl)) c.save() case introduceContactGroupCommand: @@ -1666,8 +1651,8 @@ Handle: return } - prebody := "To: " + cl[1].name - for _, to := range cl[2:] { + prebody := "To: " + cl[0].name + for _, to := range cl[1:] { prebody += ", " + to.name } prebody += "\n\n" @@ -1676,15 +1661,11 @@ Handle: c.Printf("Not OK, what now?") } - urls := c.introducePandaMessages_group(cl, true) - for i := range cl { - draft := c.newDraft([]uint64{cl[i].id}, nil, nil) - draft.cliId = c.newCliId() - draft.body = body + introducePandaMessageDesc + urls[i] - c.sendDraft(draft) - c.Printf("%s Sending introduction message %s%s%s to %s\n", termInfoPrefix, - termCliIdStart, draft.cliId.String(), termReset, cl[i].name) - } + draft := c.newDraft(nil, contactListToIdSet(cl), nil) + draft.body = body + c.sendDraft(draft) + c.Printf("%s Sending group introduction message %s%s%s to %d contacts.\n", termInfoPrefix, + termCliIdStart, draft.cliId.String(), termReset, len(cl)) c.save() case greetContactCommand: @@ -1694,7 +1675,7 @@ Handle: return } - pcs := c.parsePandaURLs(msg) + pcs := c.observeIntroductions(msg) for i, pc := range pcs { if cmd.Index == "*" || cmd.Index == pc.name || cmd.Index == fmt.Sprintf("%d", i) { @@ -1899,7 +1880,7 @@ func (c *cliClient) showInbox(msg *InboxMessage) { c.term.Write([]byte(terminalEscape(string(msgText), true /* line breaks ok */))) c.Printf("\n") - pcs := c.parsePandaURLs(msg) + pcs := c.observeIntroductions(msg) if len(pcs) > 0 { c.Printf("%s Introduced contacts. Add with greet command.\n", termPrefix) } diff --git a/client/disk/client.proto b/client/disk/client.proto index 4778e91..fb87c96 100644 --- a/client/disk/client.proto +++ b/client/disk/client.proto @@ -140,6 +140,7 @@ message Draft { optional fixed64 in_reply_to = 5; repeated protos.Message.Attachment attachments = 6; repeated protos.Message.Detachment detachments = 7; + // repeated protos.Message.Introduction = 8; } // We propose storing old, alternative, and proposed user names diff --git a/client/gui.go b/client/gui.go index 0387951..933a29b 100644 --- a/client/gui.go +++ b/client/gui.go @@ -1329,7 +1329,7 @@ func (c *guiClient) showInbox(id uint64) interface{} { attachmentPrefix = "attachment-" ) - pcs := c.parsePandaURLs(msg) + pcs := c.observeIntroductions(msg) if len(pcs) > 0 { grid := Grid{widgetBase: widgetBase{marginLeft: 25}, rowSpacing: 3} @@ -1610,8 +1610,14 @@ NextEvent: panic("invalid greet command") } contact := c.beginProposedPandaKeyExchange(pcs[i], msg.from) - c.contactsUI.Add(contact.id, contact.name, "pending", indicatorNone) - c.contactsUI.Select(contact.id) + if contact != nil { + c.contactsUI.Add(contact.id, contact.name, "pending", indicatorNone) + c.contactsUI.Select(contact.id) + } else { + fmt.Printf(" FUCK!\n") + // Internal error so go to log or set its indicator + return c.logUI() + } c.gui.Actions() <- Sensitive{name: click.name, sensitive: false} c.gui.Actions() <- SetButtonText{name: click.name, text: "Pending"} c.gui.Signal() diff --git a/client/introduce.go b/client/introduce.go index 963c14a..6771328 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -4,8 +4,11 @@ import ( "fmt" "net/url" "regexp" + "sort" "github.com/agl/pond/panda" + pond "github.com/agl/pond/protos" + "github.com/golang/protobuf/proto" ) const ( @@ -47,6 +50,13 @@ func (c *client) contactListFromIdSet(set []uint64) (ci contactList) { return } +func contactListToIdSet(cl contactList) (set []uint64) { + for _, cnt := range cl { + addIdSet(&set, cnt.id) + } + return +} + func (contact *Contact) keepSocialGraphRecords() bool { return contact.introducedBy != disableDarkWebOfTrust } @@ -79,81 +89,67 @@ func (c *client) deleteSocialGraphRecords(id uint64) { } } -func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (string, string) { +// We could make this into a tagged union of a []pond.Message_Introduction +// and a uri string if we want to support older pond clients +type Introductions []*pond.Message_Introduction + +func (c *client) introducePandaMessages_pair(cnt1, cnt2 *Contact, real bool) (Introductions, Introductions) { panda_secret := panda.NewSecretString(c.rand)[2:] - s := func(cnt *Contact) string { - v := url.Values{ - "pandaSecret": {panda_secret}, - "identity": {fmt.Sprintf("%x", cnt.theirIdentityPublic)}, - } - u := url.URL{ - Scheme: "pond-introduce", - Opaque: url.QueryEscape(cnt.name), - RawQuery: v.Encode(), + intro := func(cnt *Contact) Introductions { + i := &pond.Message_Introduction{ + Name: proto.String(cnt.name), + Identity: cnt.theirIdentityPublic[:], + PandaSecret: proto.String(panda_secret), } - return u.String() + "#" + return Introductions{i} + /* + if new protocol version { + ... above code ... + } else old protocol version { + v := url.Values{ + "pandaSecret": {panda_secret}, + "identity": {fmt.Sprintf("%x", cnt.theirIdentityPublic)}, + } + u := url.URL{ + Scheme: "pond-introduce", + Opaque: url.QueryEscape(cnt.name), + RawQuery: v.Encode(), + } + i.uri = u.String() + "#" + } + */ } if real && cnt1.keepSocialGraphRecords() && cnt2.keepSocialGraphRecords() { addIdSet(&cnt1.introducedTo, cnt2.id) addIdSet(&cnt2.introducedTo, cnt1.id) } - return s(cnt2), s(cnt1) + return intro(cnt2), intro(cnt1) } -func (c *client) introducePandaMessages(shown, hidden contactList, real bool) ([]string, []string) { +func (c *client) introducePandaMessages(shown, hidden contactList, real bool) ([]Introductions, []Introductions) { n := len(shown) + len(hidden) - var urls []string = make([]string, n) + var intros []Introductions = make([]Introductions, n) cnts := append(shown, hidden...) for i := 0; i < len(shown); i++ { for j := i + 1; j < n; j++ { ui, uj := c.introducePandaMessages_pair(cnts[i], cnts[j], real) - urls[i] += ui - urls[j] += uj + intros[i] = append(intros[i], ui...) + intros[j] = append(intros[j], uj...) } } - return urls[0:len(shown)], urls[len(shown):] + return intros[0:len(shown)], intros[len(shown):] } -func (c *client) introducePandaMessages_onemany(cnts contactList, real bool) []string { +func (c *client) introducePandaMessages_onemany(cnts contactList, real bool) []Introductions { urls1, urls2 := c.introducePandaMessages(contactList{cnts[0]}, cnts[1:], real) return append(urls1, urls2...) } -/* -func (c *client) introducePandaMessages_onemany(cnts contactList) ([]string) { - var urls []string = make([]string,len(cnts)) - cnt1 := cnts[0] - for i, cnt2 := range cnts[1:] { - // if i==0 { continue } - u1,u2 := c.introducePandaMessages_pair(cnt1,cnt2) - urls[0] += u1 - urls[i] = u2 - } - return urls -} -*/ - -func (c *client) introducePandaMessages_group(cnts contactList, real bool) []string { +func (c *client) introducePandaMessages_group(cnts contactList, real bool) []Introductions { urls, _ := c.introducePandaMessages(cnts, nil, real) return urls } -/* -func (c *client) introducePandaMessages_group(cnts contactList) ([]string) { - n := len(cnts) - var urls []string = make([]string,len(cnts)) - // for i := 0; i < n; i++ { urls[i] = "" } - for i := 0; i < n; i++ { - for j := i+1; j < n; j++ { - ui,uj := c.introducePandaMessages_pair(cnts[i],cnts[j]) - urls[i] += ui - urls[j] += uj - } - } - return urls -} -*/ - type ProposedContact struct { sharedSecret string theirIdentityPublic [32]byte @@ -162,6 +158,20 @@ type ProposedContact struct { onGreet func(*Contact) } +type ProposedContacts []ProposedContact + +func (s ProposedContacts) Len() int { + return len(s) +} + +func (s ProposedContacts) Less(i, j int) bool { + return s[i].name < s[j].name +} + +func (s ProposedContacts) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + func (c *client) fixProposedContactName(pc *ProposedContact, sender uint64) { // We should a fuzzy test for name similarity, maybe based on n-grams // or maybe a fast fuzzy spelling suggestion algorithm @@ -212,6 +222,24 @@ func (c *client) fixProposedContactName(pc *ProposedContact, sender uint64) { } } +func (c *client) checkProposedContact(pc *ProposedContact, sender uint64) { + existing, found := c.contactByIdentity(pc.theirIdentityPublic[:]) + if found && c.contacts[sender].keepSocialGraphRecords() { + pc.id = existing.id + if existing.introducedBy != sender && existing.keepSocialGraphRecords() { + addIdSet(&existing.verifiedBy, sender) + } + } + if pc.name == "" { + pc.name = fmt.Sprintf("%x", pc.theirIdentityPublic) + c.log.Printf("Empty contact name, using identity %s.", pc.name) + } + + if !found { + c.fixProposedContactName(pc, sender) + } +} + func parseKnownOpaqueURI(s string) (opaque string, vs url.Values, err error) { u, e := url.Parse(s) opaque = u.Opaque @@ -233,10 +261,7 @@ func singletonValues(values url.Values) bool { } // Finds and parses all the pond-introduce URIs in a message body. -// Returns a list of ProposedContacts from which to create add contact buttons. -// We allow contacts to be added even if they fail most checks here because -// maybe they're the legit contact and the existing one is bad. -func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContact { +func (c *client) parsePandaURLs(sender uint64, body string) []ProposedContact { var l []ProposedContact re := regexp.MustCompile("pond-introduce:([^& ?#]+)\\?([^& ?#]+)(&([^& ?#]+))*") ms := re.FindAllString(body, -1) // -1 means find all @@ -265,34 +290,42 @@ func (c *client) parsePandaURLsText(sender uint64, body string) []ProposedContac c.log.Printf("Bad public identity %s, skipping.", identity) continue } - existing, found := c.contactByIdentity(pc.theirIdentityPublic[:]) - if found && c.contacts[sender].keepSocialGraphRecords() { - pc.id = existing.id - if existing.introducedBy != sender && existing.keepSocialGraphRecords() { - addIdSet(&existing.verifiedBy, sender) - } - } - if pc.name == "" { - c.log.Printf("Empty contact name, using identity %s.", identity) - pc.name = identity - } - - if !found { - c.fixProposedContactName(&pc, sender) - } + c.checkProposedContact(&pc, sender) l = append(l, pc) } return l } -func (c *client) parsePandaURLs(msg *InboxMessage) []ProposedContact { - var body string +// Builds list of ProposedContacts from which to create greet contact buttons. +// We allow contacts to be added even if they fail most checks here because +// maybe they're the legit contact and the existing one is bad. +func (c *client) observeIntroductions(msg *InboxMessage) []ProposedContact { + var l []ProposedContact // msg.message could be nil if we're in a half paired message situation - if msg.message != nil { - body = string(msg.message.Body) + if msg.message == nil { + return l + } + + for _, intro := range msg.message.Introductions { + pc := ProposedContact{ + sharedSecret: *intro.PandaSecret, + name: *intro.Name, + } + + if len(intro.Identity) != 32 { + c.log.Printf("Bad public identity %x, skipping.", intro.Identity) + continue + } + copy(pc.theirIdentityPublic[:], intro.Identity) + + c.checkProposedContact(&pc, msg.from) + l = append(l, pc) } - return c.parsePandaURLsText(msg.from, body) + // We sort mostly just to keep the tests deterministic + sort.Sort(ProposedContacts(l)) + + return append(l, c.parsePandaURLs(msg.from, string(msg.message.Body))...) } // Add a ProposedContact using PANDA once by building panda.SharedSecret and diff --git a/client/network.go b/client/network.go index 538fc66..35fa42f 100644 --- a/client/network.go +++ b/client/network.go @@ -113,7 +113,7 @@ func (c *client) send(to *Contact, message *pond.Message) *queuedMessage { return out } -func (c *client) sendDraftTo(draft *Draft, to *Contact) (*queuedMessage, error) { +func (c *client) sendDraftTo(draft *Draft, to *Contact, intros Introductions) (*queuedMessage, error) { // Zero length bodies are ACKs. if len(draft.body) == 0 { draft.body = " " @@ -129,6 +129,8 @@ func (c *client) sendDraftTo(draft *Draft, to *Contact) (*queuedMessage, error) DetachedFiles: draft.detachments, SupportedVersion: proto.Int32(protoVersion), } + message.Introductions = intros + if err := c.sendTest(message); err != nil { return nil, err } @@ -157,16 +159,16 @@ func (c *client) sendDraft(draft *Draft) ([]*queuedMessage, error) { var outs_bad []*queuedMessage // var outs_err []error - body := draft.body - urlsIntroduce, urlsNormal := c.introducePandaMessages( + // body := draft.body + introsIntroduce, introsNormal := c.introducePandaMessages( c.contactListFromIdSet(draft.toIntroduce), c.contactListFromIdSet(draft.toNormal), true) - urls := append(urlsIntroduce, urlsNormal...) + intros := append(introsIntroduce, introsNormal...) for i, to := range append(draft.toIntroduce, draft.toNormal...) { - if len(draft.toIntroduce) > 0 { - draft.body = body + introducePandaMessageDesc + urls[i] - } - out, err := c.sendDraftTo(draft, c.contacts[to]) + // if len(draft.toIntroduce) > 0 { + // draft.body = body + introducePandaMessageDesc + urls[i] + // } + out, err := c.sendDraftTo(draft, c.contacts[to], intros[i]) if err != nil { if i == 0 { return nil, err @@ -178,7 +180,7 @@ func (c *client) sendDraft(draft *Draft) ([]*queuedMessage, error) { } outs = append(outs, out) } - draft.body = body + // draft.body = body if len(outs_bad) == 0 { return outs, nil diff --git a/protos/pond.proto b/protos/pond.proto index 104aab3..4d13f8a 100644 --- a/protos/pond.proto +++ b/protos/pond.proto @@ -280,4 +280,14 @@ message Message { // supported_version allows a client to advertise the maximum supported // version that it speaks. optional int32 supported_version = 9; + + message Introduction { + // optional fixed64 id = 1; + optional string name = 2; + optional bytes identity = 3; + optional string panda_secret = 4; + // Switch to HMAC should let us use a "manual" style key exchange + // optional kxsBytes + } + repeated Introduction introductions = 11; } From 995a3eda5face37ffebbc414eb4ec68d50595485 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 23 Jan 2015 16:54:25 +0100 Subject: [PATCH 31/36] Rebuild protos/pond.pb.go with goprotoc.sh --- protos/pond.pb.go | 842 +++++++++++++++++++++++++++------------------- 1 file changed, 498 insertions(+), 344 deletions(-) diff --git a/protos/pond.pb.go b/protos/pond.pb.go index 0248192..020f924 100644 --- a/protos/pond.pb.go +++ b/protos/pond.pb.go @@ -1,16 +1,41 @@ // Code generated by protoc-gen-go. -// source: github.com/agl/pond/protos/pond.proto +// source: protos/pond.proto // DO NOT EDIT! +/* +Package protos is a generated protocol buffer package. + +It is generated from these files: + protos/pond.proto + +It has these top-level messages: + Request + Reply + NewAccount + AccountDetails + AccountCreated + Delivery + Fetch + Fetched + ServerAnnounce + Upload + UploadReply + Download + DownloadReply + SignedRevocation + HMACSetup + HMACStrike + KeyExchange + SignedKeyExchange + Message +*/ package protos import proto "github.com/golang/protobuf/proto" -import json "encoding/json" import math "math" -// Reference proto, json, and math imports to suppress error if they are not otherwise used. +// Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal -var _ = &json.SyntaxError{} var _ = math.Inf type Reply_Status int32 @@ -34,12 +59,25 @@ const ( Reply_RESUME_PAST_END_OF_FILE Reply_Status = 21 Reply_GENERATION_REVOKED Reply_Status = 22 Reply_CANNOT_PARSE_REVOCATION Reply_Status = 23 - Reply_REGISTRATION_DISABLED Reply_Status = 24 - Reply_HMAC_KEY_ALREADY_SET Reply_Status = 25 - Reply_HMAC_NOT_SETUP Reply_Status = 26 - Reply_HMAC_INCORRECT Reply_Status = 27 - Reply_HMAC_USED Reply_Status = 28 - Reply_HMAC_REVOKED Reply_Status = 29 + // REGISTRATION_DISABLED may be returned after a NewAccount + // request to indicate the the server doesn't accept new + // registrations. + Reply_REGISTRATION_DISABLED Reply_Status = 24 + // HMAC_KEY_ALREADY_SET is returned in reply to a HMACSetup + // request if a different HMAC key has already been setup. + Reply_HMAC_KEY_ALREADY_SET Reply_Status = 25 + // HMAC_NOT_SETUP results from a delivery attempt when the + // recipient hasn't configured an HMAC key. + Reply_HMAC_NOT_SETUP Reply_Status = 26 + // HMAC_INCORRECT results from a delivery when the HMAC of the + // one-time public key doesn't validate. + Reply_HMAC_INCORRECT Reply_Status = 27 + // HMAC_USED results from a delivery when the HMAC value has + // already been used. + Reply_HMAC_USED Reply_Status = 28 + // HMAC_REVOKED results from a delivery when the HMAC value has + // been marked as revoked. + Reply_HMAC_REVOKED Reply_Status = 29 ) var Reply_Status_name = map[int32]string{ @@ -103,9 +141,6 @@ func (x Reply_Status) Enum() *Reply_Status { func (x Reply_Status) String() string { return proto.EnumName(Reply_Status_name, int32(x)) } -func (x Reply_Status) MarshalJSON() ([]byte, error) { - return json.Marshal(x.String()) -} func (x *Reply_Status) UnmarshalJSON(data []byte) error { value, err := proto.UnmarshalJSONEnum(Reply_Status_value, data, "Reply_Status") if err != nil { @@ -139,9 +174,6 @@ func (x Message_Encoding) Enum() *Message_Encoding { func (x Message_Encoding) String() string { return proto.EnumName(Message_Encoding_name, int32(x)) } -func (x Message_Encoding) MarshalJSON() ([]byte, error) { - return json.Marshal(x.String()) -} func (x *Message_Encoding) UnmarshalJSON(data []byte) error { value, err := proto.UnmarshalJSONEnum(Message_Encoding_value, data, "Message_Encoding") if err != nil { @@ -151,6 +183,8 @@ func (x *Message_Encoding) UnmarshalJSON(data []byte) error { return nil } +// Request is the client's request to the server. Only one of the optional +// messages may be present in any Request. type Request struct { NewAccount *NewAccount `protobuf:"bytes,1,opt,name=new_account" json:"new_account,omitempty"` Deliver *Delivery `protobuf:"bytes,2,opt,name=deliver" json:"deliver,omitempty"` @@ -163,66 +197,67 @@ type Request struct { XXX_unrecognized []byte `json:"-"` } -func (this *Request) Reset() { *this = Request{} } -func (this *Request) String() string { return proto.CompactTextString(this) } -func (*Request) ProtoMessage() {} +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} -func (this *Request) GetNewAccount() *NewAccount { - if this != nil { - return this.NewAccount +func (m *Request) GetNewAccount() *NewAccount { + if m != nil { + return m.NewAccount } return nil } -func (this *Request) GetDeliver() *Delivery { - if this != nil { - return this.Deliver +func (m *Request) GetDeliver() *Delivery { + if m != nil { + return m.Deliver } return nil } -func (this *Request) GetFetch() *Fetch { - if this != nil { - return this.Fetch +func (m *Request) GetFetch() *Fetch { + if m != nil { + return m.Fetch } return nil } -func (this *Request) GetUpload() *Upload { - if this != nil { - return this.Upload +func (m *Request) GetUpload() *Upload { + if m != nil { + return m.Upload } return nil } -func (this *Request) GetDownload() *Download { - if this != nil { - return this.Download +func (m *Request) GetDownload() *Download { + if m != nil { + return m.Download } return nil } -func (this *Request) GetRevocation() *SignedRevocation { - if this != nil { - return this.Revocation +func (m *Request) GetRevocation() *SignedRevocation { + if m != nil { + return m.Revocation } return nil } -func (this *Request) GetHmacSetup() *HMACSetup { - if this != nil { - return this.HmacSetup +func (m *Request) GetHmacSetup() *HMACSetup { + if m != nil { + return m.HmacSetup } return nil } -func (this *Request) GetHmacStrike() *HMACStrike { - if this != nil { - return this.HmacStrike +func (m *Request) GetHmacStrike() *HMACStrike { + if m != nil { + return m.HmacStrike } return nil } +// Reply is the server's reply to the client. type Reply struct { Status *Reply_Status `protobuf:"varint,1,opt,name=status,enum=protos.Reply_Status,def=0" json:"status,omitempty"` AccountCreated *AccountCreated `protobuf:"bytes,2,opt,name=account_created" json:"account_created,omitempty"` @@ -235,264 +270,302 @@ type Reply struct { XXX_unrecognized []byte `json:"-"` } -func (this *Reply) Reset() { *this = Reply{} } -func (this *Reply) String() string { return proto.CompactTextString(this) } -func (*Reply) ProtoMessage() {} +func (m *Reply) Reset() { *m = Reply{} } +func (m *Reply) String() string { return proto.CompactTextString(m) } +func (*Reply) ProtoMessage() {} const Default_Reply_Status Reply_Status = Reply_OK -func (this *Reply) GetStatus() Reply_Status { - if this != nil && this.Status != nil { - return *this.Status +func (m *Reply) GetStatus() Reply_Status { + if m != nil && m.Status != nil { + return *m.Status } return Default_Reply_Status } -func (this *Reply) GetAccountCreated() *AccountCreated { - if this != nil { - return this.AccountCreated +func (m *Reply) GetAccountCreated() *AccountCreated { + if m != nil { + return m.AccountCreated } return nil } -func (this *Reply) GetFetched() *Fetched { - if this != nil { - return this.Fetched +func (m *Reply) GetFetched() *Fetched { + if m != nil { + return m.Fetched } return nil } -func (this *Reply) GetAnnounce() *ServerAnnounce { - if this != nil { - return this.Announce +func (m *Reply) GetAnnounce() *ServerAnnounce { + if m != nil { + return m.Announce } return nil } -func (this *Reply) GetUpload() *UploadReply { - if this != nil { - return this.Upload +func (m *Reply) GetUpload() *UploadReply { + if m != nil { + return m.Upload } return nil } -func (this *Reply) GetDownload() *DownloadReply { - if this != nil { - return this.Download +func (m *Reply) GetDownload() *DownloadReply { + if m != nil { + return m.Download } return nil } -func (this *Reply) GetRevocation() *SignedRevocation { - if this != nil { - return this.Revocation +func (m *Reply) GetRevocation() *SignedRevocation { + if m != nil { + return m.Revocation } return nil } -func (this *Reply) GetExtraRevocations() []*SignedRevocation { - if this != nil { - return this.ExtraRevocations +func (m *Reply) GetExtraRevocations() []*SignedRevocation { + if m != nil { + return m.ExtraRevocations } return nil } +// NewAccount is a request that the client may send to the server to request a +// new account. The public identity of the connecting client will be the `name' +// of the new account. type NewAccount struct { - Generation *uint32 `protobuf:"fixed32,1,req,name=generation" json:"generation,omitempty"` - Group []byte `protobuf:"bytes,2,req,name=group" json:"group,omitempty"` - HmacKey []byte `protobuf:"bytes,3,opt,name=hmac_key" json:"hmac_key,omitempty"` - XXX_unrecognized []byte `json:"-"` + // generation contains the revocation generation for the account. The + // client should pick it at random in order to hide the number of + // revocations that the client has performed. + Generation *uint32 `protobuf:"fixed32,1,req,name=generation" json:"generation,omitempty"` + // group contains the serialised bbssig.Group for authenticating + // deliveries to this account. + Group []byte `protobuf:"bytes,2,req,name=group" json:"group,omitempty"` + // hmac_key contains an HMAC key used to authenticate delivery + // attempts. + HmacKey []byte `protobuf:"bytes,3,opt,name=hmac_key" json:"hmac_key,omitempty"` + XXX_unrecognized []byte `json:"-"` } -func (this *NewAccount) Reset() { *this = NewAccount{} } -func (this *NewAccount) String() string { return proto.CompactTextString(this) } -func (*NewAccount) ProtoMessage() {} +func (m *NewAccount) Reset() { *m = NewAccount{} } +func (m *NewAccount) String() string { return proto.CompactTextString(m) } +func (*NewAccount) ProtoMessage() {} -func (this *NewAccount) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *NewAccount) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } -func (this *NewAccount) GetGroup() []byte { - if this != nil { - return this.Group +func (m *NewAccount) GetGroup() []byte { + if m != nil { + return m.Group } return nil } -func (this *NewAccount) GetHmacKey() []byte { - if this != nil { - return this.HmacKey +func (m *NewAccount) GetHmacKey() []byte { + if m != nil { + return m.HmacKey } return nil } +// AccountDetails contains the state of an account. type AccountDetails struct { - Queue *uint32 `protobuf:"varint,1,req,name=queue" json:"queue,omitempty"` + // queue is the number of messages waiting at the server. + Queue *uint32 `protobuf:"varint,1,req,name=queue" json:"queue,omitempty"` + // max_queue is the maximum number of messages that the server will + // queue for this account. MaxQueue *uint32 `protobuf:"varint,2,req,name=max_queue" json:"max_queue,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *AccountDetails) Reset() { *this = AccountDetails{} } -func (this *AccountDetails) String() string { return proto.CompactTextString(this) } -func (*AccountDetails) ProtoMessage() {} +func (m *AccountDetails) Reset() { *m = AccountDetails{} } +func (m *AccountDetails) String() string { return proto.CompactTextString(m) } +func (*AccountDetails) ProtoMessage() {} -func (this *AccountDetails) GetQueue() uint32 { - if this != nil && this.Queue != nil { - return *this.Queue +func (m *AccountDetails) GetQueue() uint32 { + if m != nil && m.Queue != nil { + return *m.Queue } return 0 } -func (this *AccountDetails) GetMaxQueue() uint32 { - if this != nil && this.MaxQueue != nil { - return *this.MaxQueue +func (m *AccountDetails) GetMaxQueue() uint32 { + if m != nil && m.MaxQueue != nil { + return *m.MaxQueue } return 0 } +// AccountCreated is the reply to a NewAccount request. type AccountCreated struct { Details *AccountDetails `protobuf:"bytes,1,req,name=details" json:"details,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *AccountCreated) Reset() { *this = AccountCreated{} } -func (this *AccountCreated) String() string { return proto.CompactTextString(this) } -func (*AccountCreated) ProtoMessage() {} +func (m *AccountCreated) Reset() { *m = AccountCreated{} } +func (m *AccountCreated) String() string { return proto.CompactTextString(m) } +func (*AccountCreated) ProtoMessage() {} -func (this *AccountCreated) GetDetails() *AccountDetails { - if this != nil { - return this.Details +func (m *AccountCreated) GetDetails() *AccountDetails { + if m != nil { + return m.Details } return nil } +// Delivery is a request from a client to deliver a message to an account on +// this server. There's no explicit reply protobuf for this request. Success is +// indicated via |status|. type Delivery struct { - To []byte `protobuf:"bytes,1,req,name=to" json:"to,omitempty"` - GroupSignature []byte `protobuf:"bytes,2,opt,name=group_signature" json:"group_signature,omitempty"` - Generation *uint32 `protobuf:"fixed32,3,opt,name=generation" json:"generation,omitempty"` - Message []byte `protobuf:"bytes,4,req,name=message" json:"message,omitempty"` - OneTimePublicKey []byte `protobuf:"bytes,5,opt,name=one_time_public_key" json:"one_time_public_key,omitempty"` - HmacOfPublicKey *uint64 `protobuf:"fixed64,6,opt,name=hmac_of_public_key" json:"hmac_of_public_key,omitempty"` - OneTimeSignature []byte `protobuf:"bytes,7,opt,name=one_time_signature" json:"one_time_signature,omitempty"` - XXX_unrecognized []byte `json:"-"` + // The 32-byte, public identity of the target account. + To []byte `protobuf:"bytes,1,req,name=to" json:"to,omitempty"` + // A group signature of |message| proving authorisation to deliver + // messages to the account. + GroupSignature []byte `protobuf:"bytes,2,opt,name=group_signature" json:"group_signature,omitempty"` + // The current generation number in order for the server to send + // revocation updates. + Generation *uint32 `protobuf:"fixed32,3,opt,name=generation" json:"generation,omitempty"` + // The padded message to deliver. + Message []byte `protobuf:"bytes,4,req,name=message" json:"message,omitempty"` + // one_time_public_key contains an Ed25519 public key that was issued + // by the recipient in order to authenticate delivery attempts. + OneTimePublicKey []byte `protobuf:"bytes,5,opt,name=one_time_public_key" json:"one_time_public_key,omitempty"` + // hmac_of_public_key contains a 63-bit HMAC of public key using the + // HMAC key known to server and recipient. + HmacOfPublicKey *uint64 `protobuf:"fixed64,6,opt,name=hmac_of_public_key" json:"hmac_of_public_key,omitempty"` + // one_time_signature contains a signature, by public_key, of message. + OneTimeSignature []byte `protobuf:"bytes,7,opt,name=one_time_signature" json:"one_time_signature,omitempty"` + XXX_unrecognized []byte `json:"-"` } -func (this *Delivery) Reset() { *this = Delivery{} } -func (this *Delivery) String() string { return proto.CompactTextString(this) } -func (*Delivery) ProtoMessage() {} +func (m *Delivery) Reset() { *m = Delivery{} } +func (m *Delivery) String() string { return proto.CompactTextString(m) } +func (*Delivery) ProtoMessage() {} -func (this *Delivery) GetTo() []byte { - if this != nil { - return this.To +func (m *Delivery) GetTo() []byte { + if m != nil { + return m.To } return nil } -func (this *Delivery) GetGroupSignature() []byte { - if this != nil { - return this.GroupSignature +func (m *Delivery) GetGroupSignature() []byte { + if m != nil { + return m.GroupSignature } return nil } -func (this *Delivery) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *Delivery) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } -func (this *Delivery) GetMessage() []byte { - if this != nil { - return this.Message +func (m *Delivery) GetMessage() []byte { + if m != nil { + return m.Message } return nil } -func (this *Delivery) GetOneTimePublicKey() []byte { - if this != nil { - return this.OneTimePublicKey +func (m *Delivery) GetOneTimePublicKey() []byte { + if m != nil { + return m.OneTimePublicKey } return nil } -func (this *Delivery) GetHmacOfPublicKey() uint64 { - if this != nil && this.HmacOfPublicKey != nil { - return *this.HmacOfPublicKey +func (m *Delivery) GetHmacOfPublicKey() uint64 { + if m != nil && m.HmacOfPublicKey != nil { + return *m.HmacOfPublicKey } return 0 } -func (this *Delivery) GetOneTimeSignature() []byte { - if this != nil { - return this.OneTimeSignature +func (m *Delivery) GetOneTimeSignature() []byte { + if m != nil { + return m.OneTimeSignature } return nil } +// Fetch is a request to fetch a message. It may result in either a Fetched, or +// ServerAnnounce message. (Or none at all if no messages are pending.) type Fetch struct { XXX_unrecognized []byte `json:"-"` } -func (this *Fetch) Reset() { *this = Fetch{} } -func (this *Fetch) String() string { return proto.CompactTextString(this) } -func (*Fetch) ProtoMessage() {} +func (m *Fetch) Reset() { *m = Fetch{} } +func (m *Fetch) String() string { return proto.CompactTextString(m) } +func (*Fetch) ProtoMessage() {} +// Fetched is the reply to a Fetch request if the server has a message for +// delivery. type Fetched struct { - GroupSignature []byte `protobuf:"bytes,1,req,name=group_signature" json:"group_signature,omitempty"` + // group_signature is the group signature presented by the sender. + GroupSignature []byte `protobuf:"bytes,1,req,name=group_signature" json:"group_signature,omitempty"` + // generation is the generation number used for delivery. Generation *uint32 `protobuf:"fixed32,2,req,name=generation" json:"generation,omitempty"` Message []byte `protobuf:"bytes,3,req,name=message" json:"message,omitempty"` Details *AccountDetails `protobuf:"bytes,4,req,name=details" json:"details,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *Fetched) Reset() { *this = Fetched{} } -func (this *Fetched) String() string { return proto.CompactTextString(this) } -func (*Fetched) ProtoMessage() {} +func (m *Fetched) Reset() { *m = Fetched{} } +func (m *Fetched) String() string { return proto.CompactTextString(m) } +func (*Fetched) ProtoMessage() {} -func (this *Fetched) GetGroupSignature() []byte { - if this != nil { - return this.GroupSignature +func (m *Fetched) GetGroupSignature() []byte { + if m != nil { + return m.GroupSignature } return nil } -func (this *Fetched) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *Fetched) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } -func (this *Fetched) GetMessage() []byte { - if this != nil { - return this.Message +func (m *Fetched) GetMessage() []byte { + if m != nil { + return m.Message } return nil } -func (this *Fetched) GetDetails() *AccountDetails { - if this != nil { - return this.Details +func (m *Fetched) GetDetails() *AccountDetails { + if m != nil { + return m.Details } return nil } +// ServerAnnounce is a special type of reply to a Fetch request. The message +// comes from the server, rather than from another client and it's intended to +// be used for announcements from the server operator to all or some users. type ServerAnnounce struct { Message *Message `protobuf:"bytes,1,req,name=message" json:"message,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *ServerAnnounce) Reset() { *this = ServerAnnounce{} } -func (this *ServerAnnounce) String() string { return proto.CompactTextString(this) } -func (*ServerAnnounce) ProtoMessage() {} +func (m *ServerAnnounce) Reset() { *m = ServerAnnounce{} } +func (m *ServerAnnounce) String() string { return proto.CompactTextString(m) } +func (*ServerAnnounce) ProtoMessage() {} -func (this *ServerAnnounce) GetMessage() *Message { - if this != nil { - return this.Message +func (m *ServerAnnounce) GetMessage() *Message { + if m != nil { + return m.Message } return nil } @@ -503,20 +576,20 @@ type Upload struct { XXX_unrecognized []byte `json:"-"` } -func (this *Upload) Reset() { *this = Upload{} } -func (this *Upload) String() string { return proto.CompactTextString(this) } -func (*Upload) ProtoMessage() {} +func (m *Upload) Reset() { *m = Upload{} } +func (m *Upload) String() string { return proto.CompactTextString(m) } +func (*Upload) ProtoMessage() {} -func (this *Upload) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id +func (m *Upload) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Upload) GetSize() int64 { - if this != nil && this.Size != nil { - return *this.Size +func (m *Upload) GetSize() int64 { + if m != nil && m.Size != nil { + return *m.Size } return 0 } @@ -526,13 +599,13 @@ type UploadReply struct { XXX_unrecognized []byte `json:"-"` } -func (this *UploadReply) Reset() { *this = UploadReply{} } -func (this *UploadReply) String() string { return proto.CompactTextString(this) } -func (*UploadReply) ProtoMessage() {} +func (m *UploadReply) Reset() { *m = UploadReply{} } +func (m *UploadReply) String() string { return proto.CompactTextString(m) } +func (*UploadReply) ProtoMessage() {} -func (this *UploadReply) GetResume() int64 { - if this != nil && this.Resume != nil { - return *this.Resume +func (m *UploadReply) GetResume() int64 { + if m != nil && m.Resume != nil { + return *m.Resume } return 0 } @@ -544,27 +617,27 @@ type Download struct { XXX_unrecognized []byte `json:"-"` } -func (this *Download) Reset() { *this = Download{} } -func (this *Download) String() string { return proto.CompactTextString(this) } -func (*Download) ProtoMessage() {} +func (m *Download) Reset() { *m = Download{} } +func (m *Download) String() string { return proto.CompactTextString(m) } +func (*Download) ProtoMessage() {} -func (this *Download) GetFrom() []byte { - if this != nil { - return this.From +func (m *Download) GetFrom() []byte { + if m != nil { + return m.From } return nil } -func (this *Download) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id +func (m *Download) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Download) GetResume() int64 { - if this != nil && this.Resume != nil { - return *this.Resume +func (m *Download) GetResume() int64 { + if m != nil && m.Resume != nil { + return *m.Resume } return 0 } @@ -574,37 +647,40 @@ type DownloadReply struct { XXX_unrecognized []byte `json:"-"` } -func (this *DownloadReply) Reset() { *this = DownloadReply{} } -func (this *DownloadReply) String() string { return proto.CompactTextString(this) } -func (*DownloadReply) ProtoMessage() {} +func (m *DownloadReply) Reset() { *m = DownloadReply{} } +func (m *DownloadReply) String() string { return proto.CompactTextString(m) } +func (*DownloadReply) ProtoMessage() {} -func (this *DownloadReply) GetSize() int64 { - if this != nil && this.Size != nil { - return *this.Size +func (m *DownloadReply) GetSize() int64 { + if m != nil && m.Size != nil { + return *m.Size } return 0 } +// SignedRevocation is a request for the server to store an update to the group +// public key that revokes some sender. The server will reply with a revocation +// for generation x when a delivery to that generation is requested. type SignedRevocation struct { Revocation *SignedRevocation_Revocation `protobuf:"bytes,1,req,name=revocation" json:"revocation,omitempty"` Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *SignedRevocation) Reset() { *this = SignedRevocation{} } -func (this *SignedRevocation) String() string { return proto.CompactTextString(this) } -func (*SignedRevocation) ProtoMessage() {} +func (m *SignedRevocation) Reset() { *m = SignedRevocation{} } +func (m *SignedRevocation) String() string { return proto.CompactTextString(m) } +func (*SignedRevocation) ProtoMessage() {} -func (this *SignedRevocation) GetRevocation() *SignedRevocation_Revocation { - if this != nil { - return this.Revocation +func (m *SignedRevocation) GetRevocation() *SignedRevocation_Revocation { + if m != nil { + return m.Revocation } return nil } -func (this *SignedRevocation) GetSignature() []byte { - if this != nil { - return this.Signature +func (m *SignedRevocation) GetSignature() []byte { + if m != nil { + return m.Signature } return nil } @@ -615,260 +691,305 @@ type SignedRevocation_Revocation struct { XXX_unrecognized []byte `json:"-"` } -func (this *SignedRevocation_Revocation) Reset() { *this = SignedRevocation_Revocation{} } -func (this *SignedRevocation_Revocation) String() string { return proto.CompactTextString(this) } -func (*SignedRevocation_Revocation) ProtoMessage() {} +func (m *SignedRevocation_Revocation) Reset() { *m = SignedRevocation_Revocation{} } +func (m *SignedRevocation_Revocation) String() string { return proto.CompactTextString(m) } +func (*SignedRevocation_Revocation) ProtoMessage() {} -func (this *SignedRevocation_Revocation) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *SignedRevocation_Revocation) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } -func (this *SignedRevocation_Revocation) GetRevocation() []byte { - if this != nil { - return this.Revocation +func (m *SignedRevocation_Revocation) GetRevocation() []byte { + if m != nil { + return m.Revocation } return nil } +// HMACSetup can be sent by a client to establish an HMAC key if it didn't do +// so at account creation time. type HMACSetup struct { HmacKey []byte `protobuf:"bytes,1,req,name=hmac_key" json:"hmac_key,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *HMACSetup) Reset() { *this = HMACSetup{} } -func (this *HMACSetup) String() string { return proto.CompactTextString(this) } -func (*HMACSetup) ProtoMessage() {} +func (m *HMACSetup) Reset() { *m = HMACSetup{} } +func (m *HMACSetup) String() string { return proto.CompactTextString(m) } +func (*HMACSetup) ProtoMessage() {} -func (this *HMACSetup) GetHmacKey() []byte { - if this != nil { - return this.HmacKey +func (m *HMACSetup) GetHmacKey() []byte { + if m != nil { + return m.HmacKey } return nil } +// HMACStrike is used by a client to record a number of HMAC values as used. type HMACStrike struct { + // hmacs contains a number of 63-bit HMACs. The MSB is used to signal + // whether the HMAC should be considered used (0) or revoked (1). Hmacs []uint64 `protobuf:"fixed64,1,rep,packed,name=hmacs" json:"hmacs,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *HMACStrike) Reset() { *this = HMACStrike{} } -func (this *HMACStrike) String() string { return proto.CompactTextString(this) } -func (*HMACStrike) ProtoMessage() {} +func (m *HMACStrike) Reset() { *m = HMACStrike{} } +func (m *HMACStrike) String() string { return proto.CompactTextString(m) } +func (*HMACStrike) ProtoMessage() {} -func (this *HMACStrike) GetHmacs() []uint64 { - if this != nil { - return this.Hmacs +func (m *HMACStrike) GetHmacs() []uint64 { + if m != nil { + return m.Hmacs } return nil } +// KeyExchange is a message sent between clients to establish a relation. It's +// always found inside a SignedKeyExchange. type KeyExchange struct { - PublicKey []byte `protobuf:"bytes,1,req,name=public_key" json:"public_key,omitempty"` - IdentityPublic []byte `protobuf:"bytes,2,req,name=identity_public" json:"identity_public,omitempty"` - Server *string `protobuf:"bytes,3,req,name=server" json:"server,omitempty"` - Dh []byte `protobuf:"bytes,4,req,name=dh" json:"dh,omitempty"` - Dh1 []byte `protobuf:"bytes,8,opt,name=dh1" json:"dh1,omitempty"` - Group []byte `protobuf:"bytes,5,req,name=group" json:"group,omitempty"` - GroupKey []byte `protobuf:"bytes,6,req,name=group_key" json:"group_key,omitempty"` + // Ed25519 public key. + PublicKey []byte `protobuf:"bytes,1,req,name=public_key" json:"public_key,omitempty"` + // Curve25519 public key. (Used to tell the server which account to + // deliver a message to.) + // Note: in the most up-to-date revision of the Pond ratchet, this + // should be equal to |public_key|, modulo isomorphism. + IdentityPublic []byte `protobuf:"bytes,2,req,name=identity_public" json:"identity_public,omitempty"` + // The URL of this user's home server. + Server *string `protobuf:"bytes,3,req,name=server" json:"server,omitempty"` + // A Curve25519, initial Diffie-Hellman value. + Dh []byte `protobuf:"bytes,4,req,name=dh" json:"dh,omitempty"` + // dh1 contains the second, curve25519, public key if the new-form + // ratchet is being used. + Dh1 []byte `protobuf:"bytes,8,opt,name=dh1" json:"dh1,omitempty"` + // A serialised bbssig.Group. + Group []byte `protobuf:"bytes,5,req,name=group" json:"group,omitempty"` + // A bbssig.PrivateKey to authorise message delivery. + GroupKey []byte `protobuf:"bytes,6,req,name=group_key" json:"group_key,omitempty"` + // The generation number of |group|. Generation *uint32 `protobuf:"varint,7,req,name=generation" json:"generation,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *KeyExchange) Reset() { *this = KeyExchange{} } -func (this *KeyExchange) String() string { return proto.CompactTextString(this) } -func (*KeyExchange) ProtoMessage() {} +func (m *KeyExchange) Reset() { *m = KeyExchange{} } +func (m *KeyExchange) String() string { return proto.CompactTextString(m) } +func (*KeyExchange) ProtoMessage() {} -func (this *KeyExchange) GetPublicKey() []byte { - if this != nil { - return this.PublicKey +func (m *KeyExchange) GetPublicKey() []byte { + if m != nil { + return m.PublicKey } return nil } -func (this *KeyExchange) GetIdentityPublic() []byte { - if this != nil { - return this.IdentityPublic +func (m *KeyExchange) GetIdentityPublic() []byte { + if m != nil { + return m.IdentityPublic } return nil } -func (this *KeyExchange) GetServer() string { - if this != nil && this.Server != nil { - return *this.Server +func (m *KeyExchange) GetServer() string { + if m != nil && m.Server != nil { + return *m.Server } return "" } -func (this *KeyExchange) GetDh() []byte { - if this != nil { - return this.Dh +func (m *KeyExchange) GetDh() []byte { + if m != nil { + return m.Dh } return nil } -func (this *KeyExchange) GetDh1() []byte { - if this != nil { - return this.Dh1 +func (m *KeyExchange) GetDh1() []byte { + if m != nil { + return m.Dh1 } return nil } -func (this *KeyExchange) GetGroup() []byte { - if this != nil { - return this.Group +func (m *KeyExchange) GetGroup() []byte { + if m != nil { + return m.Group } return nil } -func (this *KeyExchange) GetGroupKey() []byte { - if this != nil { - return this.GroupKey +func (m *KeyExchange) GetGroupKey() []byte { + if m != nil { + return m.GroupKey } return nil } -func (this *KeyExchange) GetGeneration() uint32 { - if this != nil && this.Generation != nil { - return *this.Generation +func (m *KeyExchange) GetGeneration() uint32 { + if m != nil && m.Generation != nil { + return *m.Generation } return 0 } +// A SignedKeyExchange is a message that's sent between clients and exposed in +// the UI. It's typically found in a PEM block with type "POND KEY EXCHANGE". type SignedKeyExchange struct { - Signed []byte `protobuf:"bytes,1,req,name=signed" json:"signed,omitempty"` + // signed contains a serialised KeyExchange message. + Signed []byte `protobuf:"bytes,1,req,name=signed" json:"signed,omitempty"` + // signature contains an Ed25519 signature of |signed| by + // |signed.public_key|. Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *SignedKeyExchange) Reset() { *this = SignedKeyExchange{} } -func (this *SignedKeyExchange) String() string { return proto.CompactTextString(this) } -func (*SignedKeyExchange) ProtoMessage() {} +func (m *SignedKeyExchange) Reset() { *m = SignedKeyExchange{} } +func (m *SignedKeyExchange) String() string { return proto.CompactTextString(m) } +func (*SignedKeyExchange) ProtoMessage() {} -func (this *SignedKeyExchange) GetSigned() []byte { - if this != nil { - return this.Signed +func (m *SignedKeyExchange) GetSigned() []byte { + if m != nil { + return m.Signed } return nil } -func (this *SignedKeyExchange) GetSignature() []byte { - if this != nil { - return this.Signature +func (m *SignedKeyExchange) GetSignature() []byte { + if m != nil { + return m.Signature } return nil } +// Message is typically contained within a NaCl box that's passed between +// clients using Delivery and Fetch. type Message struct { - Id *uint64 `protobuf:"fixed64,1,req,name=id" json:"id,omitempty"` - Time *int64 `protobuf:"varint,2,req,name=time" json:"time,omitempty"` - Body []byte `protobuf:"bytes,3,req,name=body" json:"body,omitempty"` - BodyEncoding *Message_Encoding `protobuf:"varint,4,opt,name=body_encoding,enum=protos.Message_Encoding" json:"body_encoding,omitempty"` - MyNextDh []byte `protobuf:"bytes,5,opt,name=my_next_dh" json:"my_next_dh,omitempty"` - InReplyTo *uint64 `protobuf:"varint,6,opt,name=in_reply_to" json:"in_reply_to,omitempty"` - AlsoAck []uint64 `protobuf:"varint,10,rep,name=also_ack" json:"also_ack,omitempty"` - Files []*Message_Attachment `protobuf:"bytes,7,rep,name=files" json:"files,omitempty"` - DetachedFiles []*Message_Detachment `protobuf:"bytes,8,rep,name=detached_files" json:"detached_files,omitempty"` - SupportedVersion *int32 `protobuf:"varint,9,opt,name=supported_version" json:"supported_version,omitempty"` - XXX_unrecognized []byte `json:"-"` -} - -func (this *Message) Reset() { *this = Message{} } -func (this *Message) String() string { return proto.CompactTextString(this) } -func (*Message) ProtoMessage() {} - -func (this *Message) GetId() uint64 { - if this != nil && this.Id != nil { - return *this.Id + // id is generated by the sender in order for the receiver to associate + // replies. + Id *uint64 `protobuf:"fixed64,1,req,name=id" json:"id,omitempty"` + // time is the creation time of the message in epoch nanoseconds. + Time *int64 `protobuf:"varint,2,req,name=time" json:"time,omitempty"` + // body, after decoding, is a utf8 message. + Body []byte `protobuf:"bytes,3,req,name=body" json:"body,omitempty"` + BodyEncoding *Message_Encoding `protobuf:"varint,4,opt,name=body_encoding,enum=protos.Message_Encoding" json:"body_encoding,omitempty"` + // my_next_dh contains a Curve25519 public value for future messages. + MyNextDh []byte `protobuf:"bytes,5,opt,name=my_next_dh" json:"my_next_dh,omitempty"` + // in_reply_to, if set, contains the |id| value of a previous message + // sent by the recipient. + InReplyTo *uint64 `protobuf:"varint,6,opt,name=in_reply_to" json:"in_reply_to,omitempty"` + // also_ack contains message ids for other messages that are also + // acknowledged by this message. + AlsoAck []uint64 `protobuf:"varint,10,rep,name=also_ack" json:"also_ack,omitempty"` + Files []*Message_Attachment `protobuf:"bytes,7,rep,name=files" json:"files,omitempty"` + DetachedFiles []*Message_Detachment `protobuf:"bytes,8,rep,name=detached_files" json:"detached_files,omitempty"` + // supported_version allows a client to advertise the maximum supported + // version that it speaks. + SupportedVersion *int32 `protobuf:"varint,9,opt,name=supported_version" json:"supported_version,omitempty"` + Introductions []*Message_Introduction `protobuf:"bytes,11,rep,name=introductions" json:"introductions,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Message) Reset() { *m = Message{} } +func (m *Message) String() string { return proto.CompactTextString(m) } +func (*Message) ProtoMessage() {} + +func (m *Message) GetId() uint64 { + if m != nil && m.Id != nil { + return *m.Id } return 0 } -func (this *Message) GetTime() int64 { - if this != nil && this.Time != nil { - return *this.Time +func (m *Message) GetTime() int64 { + if m != nil && m.Time != nil { + return *m.Time } return 0 } -func (this *Message) GetBody() []byte { - if this != nil { - return this.Body +func (m *Message) GetBody() []byte { + if m != nil { + return m.Body } return nil } -func (this *Message) GetBodyEncoding() Message_Encoding { - if this != nil && this.BodyEncoding != nil { - return *this.BodyEncoding +func (m *Message) GetBodyEncoding() Message_Encoding { + if m != nil && m.BodyEncoding != nil { + return *m.BodyEncoding } - return 0 + return Message_RAW } -func (this *Message) GetMyNextDh() []byte { - if this != nil { - return this.MyNextDh +func (m *Message) GetMyNextDh() []byte { + if m != nil { + return m.MyNextDh } return nil } -func (this *Message) GetInReplyTo() uint64 { - if this != nil && this.InReplyTo != nil { - return *this.InReplyTo +func (m *Message) GetInReplyTo() uint64 { + if m != nil && m.InReplyTo != nil { + return *m.InReplyTo } return 0 } -func (this *Message) GetAlsoAck() []uint64 { - if this != nil { - return this.AlsoAck +func (m *Message) GetAlsoAck() []uint64 { + if m != nil { + return m.AlsoAck } return nil } -func (this *Message) GetFiles() []*Message_Attachment { - if this != nil { - return this.Files +func (m *Message) GetFiles() []*Message_Attachment { + if m != nil { + return m.Files } return nil } -func (this *Message) GetDetachedFiles() []*Message_Detachment { - if this != nil { - return this.DetachedFiles +func (m *Message) GetDetachedFiles() []*Message_Detachment { + if m != nil { + return m.DetachedFiles } return nil } -func (this *Message) GetSupportedVersion() int32 { - if this != nil && this.SupportedVersion != nil { - return *this.SupportedVersion +func (m *Message) GetSupportedVersion() int32 { + if m != nil && m.SupportedVersion != nil { + return *m.SupportedVersion } return 0 } +func (m *Message) GetIntroductions() []*Message_Introduction { + if m != nil { + return m.Introductions + } + return nil +} + type Message_Attachment struct { Filename *string `protobuf:"bytes,1,req,name=filename" json:"filename,omitempty"` Contents []byte `protobuf:"bytes,2,req,name=contents" json:"contents,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (this *Message_Attachment) Reset() { *this = Message_Attachment{} } -func (this *Message_Attachment) String() string { return proto.CompactTextString(this) } -func (*Message_Attachment) ProtoMessage() {} +func (m *Message_Attachment) Reset() { *m = Message_Attachment{} } +func (m *Message_Attachment) String() string { return proto.CompactTextString(m) } +func (*Message_Attachment) ProtoMessage() {} -func (this *Message_Attachment) GetFilename() string { - if this != nil && this.Filename != nil { - return *this.Filename +func (m *Message_Attachment) GetFilename() string { + if m != nil && m.Filename != nil { + return *m.Filename } return "" } -func (this *Message_Attachment) GetContents() []byte { - if this != nil { - return this.Contents +func (m *Message_Attachment) GetContents() []byte { + if m != nil { + return m.Contents } return nil } @@ -883,48 +1004,81 @@ type Message_Detachment struct { XXX_unrecognized []byte `json:"-"` } -func (this *Message_Detachment) Reset() { *this = Message_Detachment{} } -func (this *Message_Detachment) String() string { return proto.CompactTextString(this) } -func (*Message_Detachment) ProtoMessage() {} +func (m *Message_Detachment) Reset() { *m = Message_Detachment{} } +func (m *Message_Detachment) String() string { return proto.CompactTextString(m) } +func (*Message_Detachment) ProtoMessage() {} -func (this *Message_Detachment) GetFilename() string { - if this != nil && this.Filename != nil { - return *this.Filename +func (m *Message_Detachment) GetFilename() string { + if m != nil && m.Filename != nil { + return *m.Filename } return "" } -func (this *Message_Detachment) GetSize() uint64 { - if this != nil && this.Size != nil { - return *this.Size +func (m *Message_Detachment) GetSize() uint64 { + if m != nil && m.Size != nil { + return *m.Size } return 0 } -func (this *Message_Detachment) GetPaddedSize() uint64 { - if this != nil && this.PaddedSize != nil { - return *this.PaddedSize +func (m *Message_Detachment) GetPaddedSize() uint64 { + if m != nil && m.PaddedSize != nil { + return *m.PaddedSize } return 0 } -func (this *Message_Detachment) GetChunkSize() uint32 { - if this != nil && this.ChunkSize != nil { - return *this.ChunkSize +func (m *Message_Detachment) GetChunkSize() uint32 { + if m != nil && m.ChunkSize != nil { + return *m.ChunkSize } return 0 } -func (this *Message_Detachment) GetKey() []byte { - if this != nil { - return this.Key +func (m *Message_Detachment) GetKey() []byte { + if m != nil { + return m.Key + } + return nil +} + +func (m *Message_Detachment) GetUrl() string { + if m != nil && m.Url != nil { + return *m.Url + } + return "" +} + +type Message_Introduction struct { + // optional fixed64 id = 1; + Name *string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"` + Identity []byte `protobuf:"bytes,3,opt,name=identity" json:"identity,omitempty"` + PandaSecret *string `protobuf:"bytes,4,opt,name=panda_secret" json:"panda_secret,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Message_Introduction) Reset() { *m = Message_Introduction{} } +func (m *Message_Introduction) String() string { return proto.CompactTextString(m) } +func (*Message_Introduction) ProtoMessage() {} + +func (m *Message_Introduction) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *Message_Introduction) GetIdentity() []byte { + if m != nil { + return m.Identity } return nil } -func (this *Message_Detachment) GetUrl() string { - if this != nil && this.Url != nil { - return *this.Url +func (m *Message_Introduction) GetPandaSecret() string { + if m != nil && m.PandaSecret != nil { + return *m.PandaSecret } return "" } From 8fe6e6bee6dc2bc2e9ff81511c9b679d40f2e2f9 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Fri, 23 Jan 2015 16:56:54 +0100 Subject: [PATCH 32/36] Disable pond-introduce URI processing We probably won't use this code for a human readable introduction trick because InboxMessage keeps the raw pond.Message marshaled, Imho it's worth disabling or removing it in a seperate commit though. --- client/introduce.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/introduce.go b/client/introduce.go index 6771328..ed3b631 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -2,9 +2,9 @@ package main import ( "fmt" - "net/url" - "regexp" "sort" + // "net/url" + // "regexp" "github.com/agl/pond/panda" pond "github.com/agl/pond/protos" @@ -240,6 +240,7 @@ func (c *client) checkProposedContact(pc *ProposedContact, sender uint64) { } } +/* func parseKnownOpaqueURI(s string) (opaque string, vs url.Values, err error) { u, e := url.Parse(s) opaque = u.Opaque @@ -296,6 +297,7 @@ func (c *client) parsePandaURLs(sender uint64, body string) []ProposedContact { } return l } +*/ // Builds list of ProposedContacts from which to create greet contact buttons. // We allow contacts to be added even if they fail most checks here because @@ -324,8 +326,8 @@ func (c *client) observeIntroductions(msg *InboxMessage) []ProposedContact { } // We sort mostly just to keep the tests deterministic sort.Sort(ProposedContacts(l)) - - return append(l, c.parsePandaURLs(msg.from, string(msg.message.Body))...) + return l + // return append(l, c.parsePandaURLs(msg.from, string(msg.message.Body))...) } // Add a ProposedContact using PANDA once by building panda.SharedSecret and From 54ce14b9a18ad979529fad6e4d4a52d93d6bc0c2 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sat, 31 Jan 2015 15:12:36 +0100 Subject: [PATCH 33/36] Rename verifiedBy to reintroducedBy VerifiedBy was a poor choice for many reasons, including the fact that introductions are not a web of trust. --- client/cli.go | 4 ++-- client/client.go | 2 +- client/disk.go | 6 +++--- client/disk/client.pb.go | 6 +++--- client/disk/client.proto | 2 +- client/gui.go | 6 +++--- client/introduce.go | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/client/cli.go b/client/cli.go index 13d47fa..9d5573a 100644 --- a/client/cli.go +++ b/client/cli.go @@ -2103,9 +2103,9 @@ func (c *cliClient) showContact(contact *Contact) { cliRow{cols: []string{"Introduced By", name}}, ) } - if len(contact.verifiedBy) > 0 { + if len(contact.reintroducedBy) > 0 { table.rows = append(table.rows, - cliRow{cols: []string{"Verified By", terminalEscape(c.listContactsAndUnknowns(contact.verifiedBy), false)}}, + cliRow{cols: []string{"Reintroduced By", terminalEscape(c.listContactsAndUnknowns(contact.reintroducedBy), false)}}, ) } if len(contact.introducedTo) > 0 { diff --git a/client/client.go b/client/client.go index 9872964..ed6a58b 100644 --- a/client/client.go +++ b/client/client.go @@ -497,7 +497,7 @@ type Contact struct { ratchet *ratchet.Ratchet introducedBy uint64 - verifiedBy []uint64 + reintroducedBy []uint64 introducedTo []uint64 cliId cliId diff --git a/client/disk.go b/client/disk.go index 177c4f5..0a651e7 100644 --- a/client/disk.go +++ b/client/disk.go @@ -109,8 +109,8 @@ func (c *client) unmarshal(state *disk.State) error { if cont.IntroducedBy != nil { contact.introducedBy = *cont.IntroducedBy } - if cont.VerifiedBy != nil && len(cont.VerifiedBy) > 0 { - contact.verifiedBy = cont.VerifiedBy + if cont.ReintroducedBy != nil && len(cont.ReintroducedBy) > 0 { + contact.reintroducedBy = cont.ReintroducedBy } if cont.IntroducedTo != nil && len(cont.IntroducedTo) > 0 { contact.introducedTo = cont.IntroducedTo @@ -287,7 +287,7 @@ func (c *client) marshal() []byte { RevokedUs: proto.Bool(contact.revokedUs), IntroducedBy: proto.Uint64(contact.introducedBy), IntroducedTo: contact.introducedTo, - VerifiedBy: contact.verifiedBy, + ReintroducedBy: contact.reintroducedBy, } cont.TheirIdentityPublic = contact.theirIdentityPublic[:] diff --git a/client/disk/client.pb.go b/client/disk/client.pb.go index 5138fc3..9c9afec 100644 --- a/client/disk/client.pb.go +++ b/client/disk/client.pb.go @@ -165,7 +165,7 @@ type Contact struct { Events []*Contact_Event `protobuf:"bytes,22,rep,name=events" json:"events,omitempty"` IsPending *bool `protobuf:"varint,15,opt,name=is_pending,def=0" json:"is_pending,omitempty"` IntroducedBy *uint64 `protobuf:"fixed64,23,opt,name=introduced_by" json:"introduced_by,omitempty"` - VerifiedBy []uint64 `protobuf:"fixed64,24,rep,name=verified_by" json:"verified_by,omitempty"` + ReintroducedBy []uint64 `protobuf:"fixed64,24,rep,name=reintroduced_by" json:"reintroduced_by,omitempty"` IntroducedTo []uint64 `protobuf:"fixed64,25,rep,name=introduced_to" json:"introduced_to,omitempty"` XXX_unrecognized []byte `json:"-"` } @@ -337,9 +337,9 @@ func (m *Contact) GetIntroducedBy() uint64 { return 0 } -func (m *Contact) GetVerifiedBy() []uint64 { +func (m *Contact) GetReintroducedBy() []uint64 { if m != nil { - return m.VerifiedBy + return m.ReintroducedBy } return nil } diff --git a/client/disk/client.proto b/client/disk/client.proto index fb87c96..d3c0ece 100644 --- a/client/disk/client.proto +++ b/client/disk/client.proto @@ -73,7 +73,7 @@ message Contact { optional bool is_pending = 15 [ default = false ]; optional fixed64 introduced_by = 23; - repeated fixed64 verified_by = 24; + repeated fixed64 reintroduced_by = 24; repeated fixed64 introduced_to = 25; } diff --git a/client/gui.go b/client/gui.go index 933a29b..c95dd2a 100644 --- a/client/gui.go +++ b/client/gui.go @@ -2182,8 +2182,8 @@ func (c *guiClient) showContact(id uint64) interface{} { } entries = append(entries, nvEntry{"INTRODUCED BY", name}) } - if len(contact.verifiedBy) > 0 { - entries = append(entries, nvEntry{"VERIFIED BY", c.listContactsAndUnknowns(contact.verifiedBy)}) + if len(contact.reintroducedBy) > 0 { + entries = append(entries, nvEntry{"REINTRODUCED BY", c.listContactsAndUnknowns(contact.reintroducedBy)}) } if len(contact.introducedTo) > 0 { entries = append(entries, nvEntry{"INTRODUCED TO", c.listContactsAndUnknowns(contact.introducedTo)}) @@ -2344,7 +2344,7 @@ func (c *guiClient) showContact(id uint64) interface{} { if !click.checks["disableSocialGraph"] { c.deleteSocialGraphRecords(id) contact.introducedBy = disableDarkWebOfTrust - contact.verifiedBy = nil + contact.reintroducedBy = nil contact.introducedTo = nil } else if contact.introducedBy == disableDarkWebOfTrust { contact.introducedBy = 0 diff --git a/client/introduce.go b/client/introduce.go index ed3b631..7da303c 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -84,7 +84,7 @@ func (c *client) deleteSocialGraphRecords(id uint64) { if contact.introducedBy == id { contact.introducedBy = 0 } - removeIdSet(&contact.verifiedBy, id) + removeIdSet(&contact.reintroducedBy, id) removeIdSet(&contact.introducedTo, id) } } @@ -227,7 +227,7 @@ func (c *client) checkProposedContact(pc *ProposedContact, sender uint64) { if found && c.contacts[sender].keepSocialGraphRecords() { pc.id = existing.id if existing.introducedBy != sender && existing.keepSocialGraphRecords() { - addIdSet(&existing.verifiedBy, sender) + addIdSet(&existing.reintroducedBy, sender) } } if pc.name == "" { From 5600f63783cf52d7beaf5da99bb1e1246cec8e51 Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sun, 1 Feb 2015 03:11:07 +0100 Subject: [PATCH 34/36] Greets work if contact exists multiple times Add support for multiple contacts with the same identity key. Allow greets to contacts who revoked us too. --- client/cli.go | 20 ++++++------------ client/client.go | 13 ++---------- client/gui.go | 17 +--------------- client/introduce.go | 49 ++++++++++++++++++++++++++++++++++++--------- 4 files changed, 48 insertions(+), 51 deletions(-) diff --git a/client/cli.go b/client/cli.go index 9d5573a..9d3e2c3 100644 --- a/client/cli.go +++ b/client/cli.go @@ -1679,8 +1679,8 @@ Handle: for i, pc := range pcs { if cmd.Index == "*" || cmd.Index == pc.name || cmd.Index == fmt.Sprintf("%d", i) { - if pc.id != 0 { - c.Printf("%s Introduced contact %s is your existing contact %s\n", termPrefix, pc.name, c.contacts[pc.id].name) + if len(pc.ids) != 0 { + c.Printf("%s Introduced contact %s is your existing contact %s\n", termPrefix, pc.name, c.listContactsAndUnknowns(pc.ids)) return } c.Printf("%s Begining PANDA key exchange with %s\n", termPrefix, pc.name) @@ -1885,19 +1885,11 @@ func (c *cliClient) showInbox(msg *InboxMessage) { c.Printf("%s Introduced contacts. Add with greet command.\n", termPrefix) } for i, pc := range pcs { - s := "" - if pc.id != 0 { - s0 := "exists" - s1 := "" - if c.contacts[pc.id].isPending { - s0 = "pending" - } - if c.contacts[pc.id].name != pc.name { - s1 += "as " + c.contacts[pc.id].name - } - s = fmt.Sprintf("(%s%s)", s0, s1) + greet := c.ProposedContactGreeting(pc, "", "exists", "pending") + if len(greet) > 0 { + greet = fmt.Sprintf(" (%s)", greet) } - c.Printf("%d. %s %s\n", i, pc.name, s) + c.Printf("%d. %s %s\n", i, pc.name, greet) } } diff --git a/client/client.go b/client/client.go index ed6a58b..8e13e65 100644 --- a/client/client.go +++ b/client/client.go @@ -496,9 +496,9 @@ type Contact struct { // New ratchet support. ratchet *ratchet.Ratchet - introducedBy uint64 + introducedBy uint64 reintroducedBy []uint64 - introducedTo []uint64 + introducedTo []uint64 cliId cliId } @@ -1131,15 +1131,6 @@ func (c *client) contactByName(name string) (*Contact, bool) { return nil, false } -func (c *client) contactByIdentity(theirIdentityPublic []byte) (*Contact, bool) { - for _, contact := range c.contacts { - if bytes.Equal(contact.theirIdentityPublic[:], theirIdentityPublic) { - return contact, true - } - } - return nil, false -} - func (c *client) deleteInboxMsg(id uint64) { newInbox := make([]*InboxMessage, 0, len(c.inbox)) for _, inboxMsg := range c.inbox { diff --git a/client/gui.go b/client/gui.go index c95dd2a..412bac3 100644 --- a/client/gui.go +++ b/client/gui.go @@ -1332,23 +1332,8 @@ func (c *guiClient) showInbox(id uint64) interface{} { pcs := c.observeIntroductions(msg) if len(pcs) > 0 { grid := Grid{widgetBase: widgetBase{marginLeft: 25}, rowSpacing: 3} - for i, pc := range pcs { - var greet string - if pc.id == 0 { - greet = "Greet" - } else { - cnt := c.contacts[pc.id] - if cnt.isPending { - greet = "Pending" - } else { - greet = "Exists" - if pc.name != cnt.name { - greet += " as " + cnt.name - } - } - // Should say Verified if the contact existed previously - } + greet := c.ProposedContactGreeting(pc, "Greet", "Exists", "Pending") grid.rows = append(grid.rows, []GridE{ {1, 1, Label{ widgetBase: widgetBase{vAlign: AlignCenter, hAlign: AlignStart}, diff --git a/client/introduce.go b/client/introduce.go index 7da303c..a9c0ac2 100644 --- a/client/introduce.go +++ b/client/introduce.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "fmt" "sort" // "net/url" @@ -154,7 +155,7 @@ type ProposedContact struct { sharedSecret string theirIdentityPublic [32]byte name string - id uint64 // zero if new or failed + ids []uint64 // zero if new or failed onGreet func(*Contact) } @@ -207,6 +208,9 @@ func (c *client) fixProposedContactName(pc *ProposedContact, sender uint64) { e += fmt.Sprintf(" Do you trust %s?", c.contacts[i].name) } + if len(pc.ids) == 0 { + return + } id0 := conflict0.id pc.onGreet = func(cnt1 *Contact) { c.logEvent(cnt1, e) @@ -223,21 +227,46 @@ func (c *client) fixProposedContactName(pc *ProposedContact, sender uint64) { } func (c *client) checkProposedContact(pc *ProposedContact, sender uint64) { - existing, found := c.contactByIdentity(pc.theirIdentityPublic[:]) - if found && c.contacts[sender].keepSocialGraphRecords() { - pc.id = existing.id - if existing.introducedBy != sender && existing.keepSocialGraphRecords() { - addIdSet(&existing.reintroducedBy, sender) + if c.contacts[sender].keepSocialGraphRecords() { + for _, cnt := range c.contacts { + if bytes.Equal(cnt.theirIdentityPublic[:], pc.theirIdentityPublic[:]) && !cnt.revokedUs { + addIdSet(&pc.ids, cnt.id) + if cnt.introducedBy != sender && cnt.keepSocialGraphRecords() { + addIdSet(&cnt.reintroducedBy, sender) + } + } } } + if pc.name == "" { pc.name = fmt.Sprintf("%x", pc.theirIdentityPublic) c.log.Printf("Empty contact name, using identity %s.", pc.name) } - if !found { - c.fixProposedContactName(pc, sender) + c.fixProposedContactName(pc, sender) +} + +func (c *client) ProposedContactGreeting(pc ProposedContact, gstr, estr, pstr string) string { + if len(pc.ids) == 0 { + return gstr + } + firstNotPending := func() (uint64, bool) { + for _, id := range pc.ids { + if !c.contacts[id].isPending { + return id, true + } + } + return pc.ids[0], false + } + id, exists := firstNotPending() + greet := map[bool]string{true: estr, false: pstr}[exists] + n := c.contacts[id].name + if pc.name != n { + greet += " as " + n } + return greet + // Should say Verified if the contact existed previously + // Maybe should mention if revoked } /* @@ -337,9 +366,9 @@ func (c *client) beginProposedPandaKeyExchange(pc ProposedContact, introducedBy c.log.Printf("Unacceptably weak secret '%s'.", pc.sharedSecret) return nil } - if pc.id != 0 { + if len(pc.ids) != 0 { c.log.Printf("Attempted to add introduced contact %s, who is your existing contact %s, this is an internal error.\n", termPrefix, - pc.name, c.contacts[pc.id].name) + pc.name, c.contacts[pc.ids[0]].name) return nil } From cdc1bec458ff19ed88019f92bdf7ffc0c0b3da6e Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Sun, 8 Feb 2015 19:11:08 +0100 Subject: [PATCH 35/36] Simpler interface for sending BCCs and introducing Suggested by @leif --- client/gui.go | 144 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 35 deletions(-) diff --git a/client/gui.go b/client/gui.go index 412bac3..83b9213 100644 --- a/client/gui.go +++ b/client/gui.go @@ -495,10 +495,12 @@ func (c *guiClient) mainUI() { widgetBase: widgetBase{width: 100, name: "newcontact"}, text: "Add", }, - Button{ - widgetBase: widgetBase{width: 100, name: "introduce"}, - text: "Introduce", - }, + /* + Button{ + widgetBase: widgetBase{width: 100, name: "introduce"}, + text: "Introduce", + }, + */ }, }, }, @@ -3388,6 +3390,27 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { c.gui.Actions() <- SetChild{name: "right", child: ui} + introductionMode := 0 + if len(draft.toIntroduce) > 0 { + introductionMode = 1 + if len(draft.toNormal) > 0 { + introductionMode = 2 + } + } + introductionModeStrings := []string{ + "Blind carbon-copy all", + "Reveal & introduce all", + "Complex introducion", + } + introductionModeParse := func(ims string) int { + for i, s := range introductionModeStrings { + if s == ims { + return i + } + } + panic("unreachable") + } + toBoxName := func(s string, i uint64) string { return fmt.Sprintf("to-box-%s-%x", s, i) } @@ -3403,20 +3426,33 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { return } toBoxAddExplainId = id - c.gui.Actions() <- Append{ - name: toBoxName("entry", id), - children: []Widget{ - Button{ - widgetBase: widgetBase{ - name: "explain-introductions", - font: "Liberation Sans 8", - foreground: colorBlue, - padding: 10, - width: 1, + if introductionMode == 2 { + c.gui.Actions() <- Append{ + name: toBoxName("entry", id), + children: []Widget{ + Button{ + widgetBase: widgetBase{ + name: "explain-introductions", + font: "Liberation Sans 8", + foreground: colorBlue, + padding: 10, + width: 1, + }, + text: "?", }, - text: "i", }, - }, + } + } else { + c.gui.Actions() <- Append{ + name: toBoxName("entry", id), + children: []Widget{ + Combo{ + widgetBase: widgetBase{name: "introduction-mode"}, + labels: introductionModeStrings, + preSelected: introductionModeStrings[introductionMode], + }, + }, + } } } toBoxAddEntry := func(id uint64, introduce bool) { @@ -3427,29 +3463,33 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { addIdSet(&draft.toNormal, id) removeIdSet(&draft.toIntroduce, id) } + children := []Widget{ + Entry{ + widgetBase: widgetBase{insensitive: true}, + text: c.contacts[id].name, + }, + Button{ + widgetBase: widgetBase{name: toBoxName("remove", id), font: "Liberation Sans 8"}, + image: indicatorRemove, + }, + } + if introductionMode == 2 { + children = append(children, + CheckButton{ + widgetBase: widgetBase{ + name: toBoxName("introduce", id), + padding: 10, + }, + checked: introduce, + text: "Introduce", + }) + } c.gui.Actions() <- Append{ name: "to-box", children: []Widget{ HBox{ widgetBase: widgetBase{name: toBoxName("entry", id)}, - children: []Widget{ - Entry{ - widgetBase: widgetBase{insensitive: true}, - text: c.contacts[id].name, - }, - Button{ - widgetBase: widgetBase{name: toBoxName("remove", id), font: "Liberation Sans 8"}, - image: indicatorRemove, - }, - CheckButton{ - widgetBase: widgetBase{ - name: toBoxName("introduce", id), - padding: 10, - }, - checked: introduce, - text: "Introduce", - }, - }, + children: children, }, }, } @@ -3713,7 +3753,12 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { if !ok { panic("unreachable") } - introduce := len(draft.toIntroduce) > 0 && len(draft.toNormal) == 0 + introduce := false + if introductionMode == 1 { + introduce = true + } else if introductionMode == 2 { + introduce = len(draft.toIntroduce) > 0 && len(draft.toNormal) == 0 + } toBoxAddEntry(contact.id, introduce) toBoxUpdateCombo() updateUsage() @@ -3743,6 +3788,35 @@ func (c *guiClient) composeUI(draft *Draft) interface{} { c.gui.Signal() continue } + if click.name == "introduction-mode" { + im := introductionModeParse(click.combos["introduction-mode"]) + if introductionMode == im { + continue + } + introductionMode = im + if im == 2 { + // We could empty and repopulate the "to-box" but + // toBoxUpdateCombo() won't do it directly. + draft.body = click.textViews["body"] + c.save() + return c.introduceUI(draft) + } else { + // draft.{toNormal,toIntroduce} are disjoint + toAll := append(draft.toNormal, draft.toIntroduce...) + if im == 0 { + draft.toNormal = toAll + draft.toIntroduce = nil + } else if im == 1 { + draft.toNormal = nil + draft.toIntroduce = toAll + } + updateUsage() + // updateSend() + } + c.gui.Signal() + c.save() + continue + } const toIntroducePrefix = "to-box-introduce-" if strings.HasPrefix(click.name, toIntroducePrefix) { id, err := strconv.ParseUint(click.name[len(toIntroducePrefix):], 16, 64) From 55505e7b9c398a5d3b0b50fece12bc77d5eb14ab Mon Sep 17 00:00:00 2001 From: Jeff Burdges Date: Wed, 25 Mar 2015 07:32:09 -0400 Subject: [PATCH 36/36] Do not display client version if we do not know it We've always had protoVersion==1 so Contact.supportedVersion==0 means we've never recieved any messages and do not know their client vesion. --- client/cli.go | 8 ++++++-- client/gui.go | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/cli.go b/client/cli.go index 9d3e2c3..06429ca 100644 --- a/client/cli.go +++ b/client/cli.go @@ -2078,13 +2078,17 @@ func (c *cliClient) showContact(contact *Contact) { rows: []cliRow{ cliRow{cols: []string{"Name", terminalEscape(contact.name, false)}}, cliRow{cols: []string{"Server", terminalEscape(contact.theirServer, false)}}, - cliRow{cols: []string{"Generation", fmt.Sprintf("%d", contact.generation)}}, cliRow{cols: []string{"Public key", fmt.Sprintf("%x", contact.theirPub[:])}}, cliRow{cols: []string{"Identity key", fmt.Sprintf("%x", contact.theirIdentityPublic[:])}}, - cliRow{cols: []string{"Client version", fmt.Sprintf("%d", contact.supportedVersion)}}, + cliRow{cols: []string{"Generation", fmt.Sprintf("%d", contact.generation)}}, }, } + if contact.supportedVersion > 0 { + table.rows = append(table.rows, + cliRow{cols: []string{"Client version", fmt.Sprintf("%d", contact.supportedVersion)}} ) + } // contact.supportedVersion == 0 means never recieved any messages + if contact.introducedBy != 0 { cnt, ok := c.contacts[contact.introducedBy] name := "Unknown" diff --git a/client/gui.go b/client/gui.go index 83b9213..63e333a 100644 --- a/client/gui.go +++ b/client/gui.go @@ -2131,15 +2131,17 @@ func (c *guiClient) showContact(id uint64) interface{} { {"SERVER", contact.theirServer}, {"PUBLIC IDENTITY", fmt.Sprintf("%x", contact.theirIdentityPublic[:])}, {"PUBLIC KEY", fmt.Sprintf("%x", contact.theirPub[:])}, + {"GROUP GENERATION", fmt.Sprintf("%d", contact.generation)}, } if !allBytesZero(contact.theirLastDHPublic[:]) { entries = append(entries, nvEntry{"LAST DH", fmt.Sprintf("%x", contact.theirLastDHPublic[:])}, nvEntry{"CURRENT DH", fmt.Sprintf("%x", contact.theirCurrentDHPublic[:])}) } - entries = append(entries, - nvEntry{"GROUP GENERATION", fmt.Sprintf("%d", contact.generation)}, - nvEntry{"CLIENT VERSION", fmt.Sprintf("%d", contact.supportedVersion)}) + if contact.supportedVersion > 0 { + entries = append(entries, + nvEntry{"CLIENT VERSION", fmt.Sprintf("%d", contact.supportedVersion)}) + } // contact.supportedVersion == 0 means never recieved any messages rowName := 0 var pandaMessage string