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/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 4a195b4..06429ca 100644 --- a/client/cli.go +++ b/client/cli.go @@ -11,6 +11,7 @@ import ( "os/signal" "path/filepath" "strconv" + "strings" "sync" "syscall" "time" @@ -932,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 @@ -950,47 +958,48 @@ 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, }) } 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 +1021,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) @@ -1028,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 @@ -1108,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) } @@ -1134,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 @@ -1252,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 { @@ -1299,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 { @@ -1318,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: @@ -1575,8 +1592,7 @@ Handle: id: c.randId(), cliId: c.newCliId(), } - - c.newKeyExchange(contact) + c.initSocialGraphRecords(contact) stack := &panda.CardStack{ NumDecks: 1, @@ -1586,21 +1602,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: @@ -1610,6 +1613,84 @@ 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 + } + + // Build from notes eventually + prebody := "To: " + contact.name + for _, to := range cl { + prebody += ", " + to.name + } + prebody += "\n\n" + body, ok := c.inputTextBlock(prebody, true) + if !ok { + c.Printf("Not OK, what now?") + } + + 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: + cl := c.inputContactList("Introduce contacts to one another.", + func(cnt *Contact) bool { return !cnt.isPending }) + if len(cl) == 0 { + return + } + + prebody := "To: " + cl[0].name + for _, to := range cl[1:] { + prebody += ", " + to.name + } + prebody += "\n\n" + body, ok := c.inputTextBlock(prebody, true) + if !ok { + c.Printf("Not OK, what now?") + } + + 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: + msg, ok := c.currentObj.(*InboxMessage) + if !ok { + c.Printf("%s Select inbox message first\n", termWarnPrefix) + return + } + + pcs := c.observeIntroductions(msg) + for i, pc := range pcs { + if cmd.Index == "*" || cmd.Index == pc.name || + cmd.Index == fmt.Sprintf("%d", i) { + 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) + c.beginProposedPandaKeyExchange(pc, msg.from) + if cmd.Index != "*" { + return + } + } + } + case retainCommand: msg, ok := c.currentObj.(*InboxMessage) if !ok { @@ -1638,28 +1719,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() @@ -1678,12 +1742,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 @@ -1709,12 +1771,74 @@ 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 *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(), + toNormal: toNormal, + toIntroduce: toIntroduce, + } + if inReplyTo != nil && inReplyTo.message != nil { + draft.inReplyTo = inReplyTo.id + 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) 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 { + c.Printf("%s Internal error, compose nolonger initializes drafts.\n", termErrPrefix) + } + + 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 !funTo("Introdiucing: ", draft.toIntroduce) || + !funTo("To: ", draft.toNormal) { + return } - draft.body = string(contents) - c.printDraftSize(draft) + body, ok := c.inputTextBlock(body0+"\n"+draft.body, true) + if !ok { + return + } + draft.body = body + c.printDraftSize(draft) c.save() } @@ -1755,6 +1879,18 @@ 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.observeIntroductions(msg) + if len(pcs) > 0 { + c.Printf("%s Introduced contacts. Add with greet command.\n", termPrefix) + } + for i, pc := range pcs { + greet := c.ProposedContactGreeting(pc, "", "exists", "pending") + if len(greet) > 0 { + greet = fmt.Sprintf(" (%s)", greet) + } + c.Printf("%d. %s %s\n", i, pc.name, greet) + } } func (c *cliClient) showOutbox(msg *queuedMessage) { @@ -1799,27 +1935,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)) } - 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 { + if len(toLine) == 0 { + toLine = fmt.Sprintf("%s To: %s\n", termHeaderPrefix, "(not specified)") + } + 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") } @@ -1839,6 +1988,77 @@ 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 *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 { + if len(listing) > 0 { + listing += "and " + } + listing += fmt.Sprintf("%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)) @@ -1858,12 +2078,38 @@ 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" + if ok { + name = terminalEscape(cnt.name, false) + } + table.rows = append(table.rows, + cliRow{cols: []string{"Introduced By", name}}, + ) + } + if len(contact.reintroducedBy) > 0 { + table.rows = append(table.rows, + cliRow{cols: []string{"Reintroduced By", terminalEscape(c.listContactsAndUnknowns(contact.reintroducedBy), 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/client.go b/client/client.go index 0d5ddef..8e13e65 100644 --- a/client/client.go +++ b/client/client.go @@ -356,6 +356,28 @@ 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 { + 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 { @@ -474,6 +496,10 @@ type Contact struct { // New ratchet support. ratchet *ratchet.Ratchet + introducedBy uint64 + reintroducedBy []uint64 + introducedTo []uint64 + cliId cliId } @@ -544,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 @@ -581,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) @@ -604,8 +631,23 @@ func (draft *Draft) usageString() (string, bool) { if err != nil { panic("error while serialising candidate Message: " + err.Error()) } + l := uint64(len(serialized)) + + // 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(uint64(len(serialized))), prettyNumber(pond.MaxSerializedMessage)) + s := fmt.Sprintf("%s of %s bytes", prettyNumber(l), prettyNumber(pond.MaxSerializedMessage)) return s, len(serialized) > pond.MaxSerializedMessage } @@ -653,7 +695,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, @@ -1086,7 +1128,6 @@ func (c *client) contactByName(name string) (*Contact, bool) { return contact, true } } - return nil, false } @@ -1189,10 +1230,10 @@ 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) } + c.deleteSocialGraphRecords(contact.id) c.queueMutex.Lock() var newQueue []*queuedMessage @@ -1290,6 +1331,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/client_test.go b/client/client_test.go index 07acac7..56659d3 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"}, } @@ -1626,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 { @@ -1723,9 +1727,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 +1742,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 +1835,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") } } @@ -2429,3 +2442,128 @@ 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) } + + mp := panda.NewSimpleMeetingPlace() + newMeetingPlace := func() panda.MeetingPlace { + return mp + } + + clients := []*TestClient{} + for i := 0; i < 4; i++ { + client, err := NewTestClient(t, clientName(i), nil) + if err != nil { + t.Fatal(err) + } + client.newMeetingPlace = newMeetingPlace + 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], + "client 0", clientName(i), server) + } + + composeMessageStart(clients[0]) + 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++ { + transmitMessage(clients[0], false) + } + + for _, client := range clients[1:] { + from, _ := fetchMessage(client) + if from != "client 0" { + t.Fatalf("message from %s, expected client 0", from) + } + } + + var wg sync.WaitGroup + 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() + }() + } + + wg.Add(2) + doGreet(clients[1], 0) + // 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[3], "client 1", "client 3") + + // verifyUnpaired(clients[2],clients[3]) +} diff --git a/client/disk.go b/client/disk.go index 8406609..0a651e7 100644 --- a/client/disk.go +++ b/client/disk.go @@ -106,8 +106,26 @@ func (c *client) unmarshal(state *disk.State) error { } } + if cont.IntroducedBy != nil { + contact.introducedBy = *cont.IntroducedBy + } + if cont.ReintroducedBy != nil && len(cont.ReintroducedBy) > 0 { + contact.reintroducedBy = cont.ReintroducedBy + } + if cont.IntroducedTo != nil && len(cont.IntroducedTo) > 0 { + contact.introducedTo = cont.IntroducedTo + } + 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 +147,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) @@ -237,9 +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 = *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 } @@ -267,7 +285,12 @@ 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, + ReintroducedBy: contact.reintroducedBy, } + + cont.TheirIdentityPublic = contact.theirIdentityPublic[:] if !contact.isPending { cont.MyGroupKey = contact.myGroupKey.Marshal() cont.TheirGroup = contact.myGroupKey.Group.Marshal() @@ -275,7 +298,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[:] } @@ -366,10 +388,10 @@ func (c *client) marshal() []byte { Attachments: draft.attachments, Detachments: draft.detachments, Created: proto.Int64(draft.created.Unix()), + ToNormal: draft.toNormal, + ToIntroduce: draft.toIntroduce, } - if draft.to != 0 { - m.To = proto.Uint64(draft.to) - } + if draft.inReplyTo != 0 { m.InReplyTo = proto.Uint64(draft.inReplyTo) } diff --git a/client/disk/client.pb.go b/client/disk/client.pb.go index 1f29160..9c9afec 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"` + 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:"-"` } -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) GetReintroducedBy() []uint64 { + if m != nil { + return m.ReintroducedBy + } + 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 } @@ -707,7 +756,8 @@ func (this *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"` @@ -715,55 +765,62 @@ 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) 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 (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 +842,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 +944,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 } diff --git a/client/disk/client.proto b/client/disk/client.proto index ff47104..d3c0ece 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 reintroduced_by = 24; + repeated fixed64 introduced_to = 25; } message RatchetState { @@ -130,13 +134,23 @@ 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; repeated protos.Message.Detachment detachments = 7; + // repeated protos.Message.Introduction = 8; } +// 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; 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) +} 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() diff --git a/client/gui.go b/client/gui.go index 7d96a5f..63e333a 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 @@ -61,6 +62,7 @@ const ( uiStateMain uiStateCreateAccount uiStateCreatePassphrase + uiStateIntroduceContact uiStateNewContact uiStateNewContact2 uiStateShowContact @@ -142,7 +144,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 } @@ -388,6 +390,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{ @@ -489,6 +495,12 @@ func (c *guiClient) mainUI() { widgetBase: widgetBase{width: 100, name: "newcontact"}, text: "Add", }, + /* + Button{ + widgetBase: widgetBase{width: 100, name: "introduce"}, + text: "Introduce", + }, + */ }, }, }, @@ -590,12 +602,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{ @@ -653,7 +662,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) @@ -663,8 +672,10 @@ func (c *guiClient) mainUI() { switch click.name { case "newcontact": nextEvent = c.newContactUI(nil) + case "introduce": + nextEvent = c.introduceUI(nil) case "compose": - nextEvent = c.composeUI(nil, nil) + nextEvent = c.composeUI(nil) } } } @@ -1311,6 +1322,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 +1331,37 @@ func (c *guiClient) showInbox(id uint64) interface{} { attachmentPrefix = "attachment-" ) + pcs := c.observeIntroductions(msg) + if len(pcs) > 0 { + grid := Grid{widgetBase: widgetBase{marginLeft: 25}, rowSpacing: 3} + for i, pc := range pcs { + greet := c.ProposedContactGreeting(pc, "Greet", "Exists", "Pending") + 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 +1591,24 @@ 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], msg.from) + 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() + continue case strings.HasPrefix(click.name, attachmentPrefix): i, _ := strconv.Atoi(click.name[len(attachmentPrefix):]) c.gui.Actions() <- FileOpen{ @@ -1594,9 +1655,10 @@ 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(nil, msg) + return c.composeUI(c.newDraftUI(nil, nil, msg)) case click.name == "delete": c.inboxUI.Remove(msg.id) c.deleteInboxMsg(msg.id) @@ -1774,7 +1836,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" { @@ -2069,15 +2131,18 @@ 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 @@ -2095,6 +2160,24 @@ func (c *guiClient) showContact(id uint64) interface{} { entries = append(entries, nvEntry{"KEY EXCHANGE", string(out.Bytes())}) } + rowDarkWebOfTrust := len(entries) + entries = append(entries, nvEntry{"", ""}) + + if contact.introducedBy != 0 && contact.introducedBy != disableDarkWebOfTrust { + cnt, ok := c.contacts[contact.introducedBy] + name := "Unknown" + if ok { + name = cnt.name + } + entries = append(entries, nvEntry{"INTRODUCED BY", name}) + } + 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)}) + } + if len(contact.events) > 0 { eventsText := "" for i, event := range contact.events { @@ -2121,13 +2204,30 @@ func (c *guiClient) showContact(id uint64) interface{} { text: "Delete", }}, }, + {{1, 1, nil}}, + { + {1, 1, Button{ + widgetBase: widgetBase{ + name: "composeTo", + insensitive: contact.isPending || contact.revokedUs, + }, + text: "Compose", + }}, + {1, 1, Button{ + widgetBase: widgetBase{ + name: "introduceTo", + insensitive: contact.isPending || contact.revokedUs, + }, + text: "Introduce", + }}, + }, }, } 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{ @@ -2146,6 +2246,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() @@ -2206,27 +2319,221 @@ 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) c.gui.Actions() <- UIState{uiStateContactNameChanged} c.gui.Signal() - c.save() + + case "disableSocialGraph": + if !click.checks["disableSocialGraph"] { + c.deleteSocialGraphRecords(id) + contact.introducedBy = disableDarkWebOfTrust + contact.reintroducedBy = 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": + return c.introduceUI(c.newDraftUI(nil, []uint64{contact.id}, nil)) } } panic("unreachable") } +func (c *guiClient) introduceUI(draft *Draft) interface{} { + if draft == nil { + draft = c.newDraftUI(nil, nil, nil) + c.draftsUI.SetLine(draft.id, "Introduction") + } + + 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 + } + cl = append(cl, contact) + if len(contact.name) > nameLen { + nameLen = len(contact.name) + } + } + var perLine int = 100 / (nameLen + 5) + if perLine > 10 { + perLine = 10 + } + + 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, + } + } + + 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", + }, + Button{ + widgetBase: widgetBase{name: "draft", insensitive: false, padding: 5}, + text: "Draft", + }, + }, + } + // 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() + + ccbClick := func(to *contactCheckBoxes, click Click) bool { + if !strings.HasPrefix(click.name, to.prefix) { + return false + } + 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 + } + + for { + event, wanted := c.nextEvent(0) + if wanted { + return event + } + + // if update, ok := event.(Update); ok { + // updateUsage() + // draft.body = update.text + // c.gui.Signal() + // continue + // } + + click, ok := event.(Click) + if !ok { + continue + } + + if ccbClick(&toIntroduce, click) { + continue + } + if ccbClick(&toNormal, click) { + continue + } + + 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 + } + } + } +} + func (c *guiClient) newContactUI(contact *Contact) interface{} { var name string existing := contact != nil @@ -2366,6 +2673,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} @@ -2777,19 +3085,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 +3098,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) } @@ -2949,104 +3245,54 @@ 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(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) + return draft } -func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} { - if draft != nil && inReplyTo != nil { - panic("draft and inReplyTo both set") - } - - var contactNames []string - for _, contact := range c.contacts { - if !contact.isPending && !contact.revokedUs { - contactNames = append(contactNames, contact.name) - } - } - - var preSelected string - if inReplyTo != nil { - if from, ok := c.contacts[inReplyTo.from]; ok { - preSelected = from.name - } +func (c *guiClient) composeUI(draft *Draft) interface{} { + if draft == nil { + draft = c.newDraftUI(nil, nil, nil) } 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 } - - if draft != nil && draft.inReplyTo != 0 { - for _, msg := range c.inbox { - if msg.id == draft.inReplyTo { - inReplyTo = msg - break - } - } + for i := range draft.detachments { + detachments[c.randId()] = i } - 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 + // We modify overSize in updateSend() and updateUsage() + // We modify usageMessage in updateUsage() but do not use it currently + usageMessage, overSize := c.usageString(draft) 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}, }, }, }, @@ -3060,7 +3306,7 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} }, Label{ widgetBase: widgetBase{name: "usage"}, - text: initialUsageMessage, + text: usageMessage, }, }, }, @@ -3092,7 +3338,7 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) 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{ @@ -3146,6 +3392,196 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) 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) + } + 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 + 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: "?", + }, + }, + } + } 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) { + if introduce { + addIdSet(&draft.toIntroduce, id) + removeIdSet(&draft.toNormal, id) + } else { + 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: children, + }, + }, + } + if len(draft.toIntroduce)+len(draft.toNormal) == 1 { + toBoxAddExplain() + } + } + 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) } @@ -3176,9 +3612,19 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} } } - detachmentUI := ComposeDetachmentUI{draft, detachments, c.gui, func() { - overSize = c.updateUsage(validContactSelected, draft) - }} + updateUsage := func() { + // 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 { + 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() @@ -3190,7 +3636,7 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} } if update, ok := event.(Update); ok { - overSize = c.updateUsage(validContactSelected, draft) + updateUsage() draft.body = update.text c.gui.Signal() continue @@ -3260,7 +3706,7 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) 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 { @@ -3289,38 +3735,116 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) 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 := 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() + // 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)} + if id == toBoxAddExplainId { + toBoxAddExplain() + } + toBoxUpdateCombo() + updateUsage() + // updateSend() + introduceSensitivity() + c.draftsUI.SetLine(draft.id, c.sideDraftRecipients(draft)) + 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.draftsUI.SetLine(draft.id, selected) - if validContactSelected && !overSize { - c.gui.Actions() <- Sensitive{name: "send", sensitive: true} - c.gui.Signal() + 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) + if _, ok := c.contacts[id]; err != nil || !ok { + panic(click.name) + } + introduce := click.checks[click.name] + if introduce { + addIdSet(&draft.toIntroduce, id) + removeIdSet(&draft.toNormal, id) + } else { + addIdSet(&draft.toNormal, id) + removeIdSet(&draft.toIntroduce, id) } + updateUsage() + 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. @@ -3343,7 +3867,7 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} draft.detachments = append(draft.detachments[:index], draft.detachments[index+1:]...) delete(detachments, id) } - overSize = c.updateUsage(validContactSelected, draft) + updateUsage() c.gui.Signal() continue } @@ -3385,50 +3909,66 @@ func (c *guiClient) composeUI(draft *Draft, inReplyTo *InboxMessage) interface{} c.gui.Signal() } - 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 + if click.name == "explain-introductions" { + draft.body = click.textViews["body"] + c.save() + return c.introduceUI(draft) } - for _, contact := range c.contacts { - if contact.name == toName { - draft.to = contact.id - break + if click.name == "send" { + if len(draft.toIntroduce)+len(draft.toNormal) == 0 { + panic("Can't send messge with no recipients") } - } - if inReplyTo != nil { - draft.inReplyTo = inReplyTo.message.GetId() + draft.body = click.textViews["body"] + if r, err := c.composeUIsend(draft); err == nil { + return r + } } - draft.body = click.textViews["body"] + } - id, created, 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) - } + return nil +} - c.draftsUI.Remove(draft.id) - delete(c.drafts, draft.id) +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 + } - c.save() + for _, msg := range messages { + c.outboxUI.Add(msg.id, c.ContactName(msg.to), + msg.created.Format(shortTimeFormat), indicatorRed) + } - c.outboxUI.Select(id) - return c.showOutbox(id) + 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 diff --git a/client/introduce.go b/client/introduce.go new file mode 100644 index 0000000..a9c0ac2 --- /dev/null +++ b/client/introduce.go @@ -0,0 +1,402 @@ +package main + +import ( + "bytes" + "fmt" + "sort" + // "net/url" + // "regexp" + + "github.com/agl/pond/panda" + pond "github.com/agl/pond/protos" + "github.com/golang/protobuf/proto" +) + +const ( + disableDarkWebOfTrust = 0xFFFFFFFFFFFFFFFF + introducePandaMessageDesc = "\n---- Introduction URIs for proposed new contacts ----\n" +) + +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 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 contactListToIdSet(cl contactList) (set []uint64) { + for _, cnt := range cl { + addIdSet(&set, cnt.id) + } + 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.reintroducedBy, id) + removeIdSet(&contact.introducedTo, id) + } +} + +// 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:] + intro := func(cnt *Contact) Introductions { + i := &pond.Message_Introduction{ + Name: proto.String(cnt.name), + Identity: cnt.theirIdentityPublic[:], + PandaSecret: proto.String(panda_secret), + } + 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 intro(cnt2), intro(cnt1) +} + +func (c *client) introducePandaMessages(shown, hidden contactList, real bool) ([]Introductions, []Introductions) { + n := len(shown) + len(hidden) + 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) + intros[i] = append(intros[i], ui...) + intros[j] = append(intros[j], uj...) + } + } + return intros[0:len(shown)], intros[len(shown):] +} + +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_group(cnts contactList, real bool) []Introductions { + urls, _ := c.introducePandaMessages(cnts, nil, real) + return urls +} + +type ProposedContact struct { + sharedSecret string + theirIdentityPublic [32]byte + name string + ids []uint64 // zero if new or failed + 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 + // https://github.com/sajari/fuzzy + // or even JaroWinkler or Levenshtein from + // "github.com/antzucaro/matchr" here : + // https://godoc.org/github.com/antzucaro/matchr#JaroWinkler + s := "" + conflict0, 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 + + 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) + } + + if len(pc.ids) == 0 { + return + } + 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) + } +} + +func (c *client) checkProposedContact(pc *ProposedContact, sender uint64) { + 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) + } + + 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 +} + +/* +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. +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 + for _, m := range ms { + opaque, vs, err := parseKnownOpaqueURI(m) + if err != nil || !singletonValues(vs) { + c.log.Printf("Malformed pond-introduce: URI : %s", m) + continue + } + + var pc ProposedContact + pc.name, err = url.QueryUnescape(opaque) + if err != nil { + c.log.Printf("Malformed pond-introduce: URI : %s", m) + continue + } + + 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 + } + + c.checkProposedContact(&pc, sender) + l = append(l, pc) + } + 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 +// 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 { + 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) + } + // We sort mostly just to keep the tests deterministic + sort.Sort(ProposedContacts(l)) + return l + // return append(l, c.parsePandaURLs(msg.from, string(msg.message.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, introducedBy uint64) *Contact { + if len(pc.sharedSecret) == 0 || !panda.IsAcceptableSecretString(pc.sharedSecret) { + c.log.Printf("Unacceptably weak secret '%s'.", pc.sharedSecret) + return nil + } + 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.ids[0]].name) + return nil + } + + contact := &Contact{ + name: pc.name, + isPending: true, + id: c.randId(), + 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) + } + if pc.onGreet != nil { + pc.onGreet(contact) + } + + stack := &panda.CardStack{ + NumDecks: 1, + } + secret := panda.SharedSecret{ + Secret: pc.sharedSecret, + Cards: *stack, + } + c.newKeyExchange(contact) + c.beginPandaKeyExchange(contact, secret) + return contact +} diff --git a/client/network.go b/client/network.go index 673e3c0..35fa42f 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,32 +110,39 @@ 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, intros Introductions) (*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), } + message.Introductions = intros + + if err := c.sendTest(message); err != nil { + return nil, err + } 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 { @@ -139,8 +151,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 + introsIntroduce, introsNormal := c.introducePandaMessages( + c.contactListFromIdSet(draft.toIntroduce), + c.contactListFromIdSet(draft.toNormal), true) + 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], intros[i]) + 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. 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 + 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 "" } 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; }