diff --git a/.gitignore b/.gitignore index 8223752..96785e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.out .idea +*private_key* +*.messages +*.test diff --git a/editor.go b/editor.go deleted file mode 100644 index 647c96d..0000000 --- a/editor.go +++ /dev/null @@ -1,106 +0,0 @@ -package main - -import ( - "strings" - - "github.com/jroimartin/gocui" -) - -func editor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { - //state.HideHelp = true - - //if state.Mode == modeEscape { - // escEditor(v, key, ch, mod) - // return - //} - - if ch != 0 && mod == 0 { - v.EditWrite(ch) - } - - switch key { - case gocui.KeyEsc: - //state.Mode = modeEscape - //state.KeepAutoscrolling = true - - // Space, backspace, Del - case gocui.KeySpace: - v.EditWrite(' ') - case gocui.KeyBackspace, gocui.KeyBackspace2: - v.EditDelete(true) - moveAhead(v) - case gocui.KeyDelete: - v.EditDelete(false) - - // Cursor movement - case gocui.KeyArrowLeft: - v.MoveCursor(-1, 0, false) - moveAhead(v) - case gocui.KeyArrowRight: - x, _ := v.Cursor() - x2, _ := v.Origin() - x += x2 - buf := v.Buffer() - // I don't know really how this works, this was mostly obtained through trial - // and error. Anyway, this system impedes going on a newline by moving right. - // This is usually possible because once you write something to the buffer - // it automatically adds " \n", which is two characters. Sooo yeah. - if buf != "" && len(buf) > (x+2) { - v.MoveCursor(1, 0, false) - } - - case gocui.KeyEnter: - buf := v.Buffer() - v.Clear() - v.SetCursor(0, 0) - - if buf != "" { - buf = buf[:len(buf)-1] - } - if strings.TrimSpace(buf) != "" { - //state.PushAction(buf) - //state.ActionIndex = -1 - } - - enterActionConnect(buf) - - } -} - -func setText(v *gocui.View, text string) { - v.Clear() - // Why are we doing this? Because normally when you write a line - // gocui adds " \n" at the end of it. Whe clearing and adding, though, - // the space isn't added back. - v.Write([]byte(text + " ")) - v.SetCursor(len(text), 0) -} - -// moveAhead displays the next 10 characters when moving backwards, -// in order to see where we're moving or what we're deleting. -func moveAhead(v *gocui.View) { - cX, _ := v.Cursor() - oX, _ := v.Origin() - if cX < 10 && oX > 0 { - newOX := oX - 10 - forward := 10 - if newOX < 0 { - forward += newOX - newOX = 0 - } - v.SetOrigin(newOX, 0) - v.MoveCursor(forward, 0, false) - } -} - -func enterActionConnect(buf string) { - //log.Printf("Connecting: %s",buf) - connect(buf) -} - -func moveDown(v *gocui.View) { - _, yPos := v.Cursor() - if _, err := v.Line(yPos + 1); err == nil { - v.MoveCursor(0, 1, false) - } -} diff --git a/main.go b/main.go deleted file mode 100644 index b8efabb..0000000 --- a/main.go +++ /dev/null @@ -1,231 +0,0 @@ -package main - -import ( - //"fmt" - "git.mascherari.press/cwtch/client" - "git.mascherari.press/cwtch/ui" - //"git.mascherari.press/cwtch/server" - //"github.com/fatih/color" - "github.com/jroimartin/gocui" - "log" - "os" - "strconv" - "strings" - //"time" -) - -var cwtch_client *client.CwtchPeer -var state ui.State - -func run_client(hostname string) { - //cwtch_client.In = make(chan string) - //cwtch_client.Out = make(chan string) - cwtch_client = client.NewCwtchPeer("sarah") - go cwtch_client.Listen() -} - -func fetch_messages(g *gocui.Gui) { - for { - message := <-cwtch_client.Log - screen := state.GetScreen(0) - screen.AppendToLog(message) - state.Render() - } - - /*for { - message := <-cwtch_client.Out - parts := strings.SplitN(message, " ", 2) - if len(parts) == 2 && len(parts[0]) == 16 { - colorPurple := color.New(color.FgMagenta).Fprint - colorYellow := color.New(color.FgGreen).Fprint - if v, err := g.SetCurrentView("out"); err == nil { - Update(func(*gocui.Gui) error { - t := time.Now() - h := strconv.Itoa(t.Hour()) - if len(h) < 2 { - h = "0" + h - } - m := strconv.Itoa(t.Minute()) - if len(m) < 2 { - m = "0" + m - } - s := strconv.Itoa(t.Second()) - if len(s) < 2 { - s = "0" + s - } - colorYellow(v, "["+h+":"+m+":"+s+"] ") - colorPurple(v, fmt.Sprintf("%s: ", parts[0])) - v.Write([]byte(fmt.Sprintf("%s\n", parts[1]))) - return nil - }) - } - } - }*/ -} - -func main() { - - if len(os.Args) != 2 { - os.Exit(1) - } - - //if os.Args[1] == "server" { - // cwtchserver := new(server.Server) - // cwtchserver.Init() - // cwtchserver.Run("server_key") - // os.Exit(0) - //} - - run_client(os.Args[1]) - - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - log.Panicln(err) - } - defer g.Close() - - state.NewScreen("Welcome", false) - state.NewScreen("#anonymity", true) - state.NewScreen("Sarah", false) - state.Gui = g - //state.ExecuteFunc = g.Update - - g.SetManagerFunc(layout) - g.Cursor = true - g.InputEsc = true - g.Mouse = false - - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - log.Panicln(err) - } - - if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, next); err != nil { - log.Panicln(err) - } - - /** if err := g.SetKeybinding("", - gocui.MouseWheelUp, - gocui.ModNone, ScrollUp); err != nil { - log.Panicln(err) - } - - if err := g.SetKeybinding("", - gocui.MouseWheelDown, - gocui.ModNone, ScrollDown); err != nil { - log.Panicln(err) - }**/ - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) - } -} - -var SLTWriter *gocui.View -var Update func(func(*gocui.Gui) error) - -func layout(g *gocui.Gui) error { - // Set when doing a double-esc - //if state.ShouldQuit { - // return gocui.ErrQuit - //} - Update = g.Update - maxX, maxY := g.Size() - if v, err := g.SetView("cmd", 1, maxY-2, maxX, maxY); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Frame = false - v.Editable = true - v.Editor = gocui.EditorFunc(editor) - v.Clear() - } - - if v, err := g.SetView("menu", -1, maxY-4, maxX, maxY-2); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.FgColor = gocui.Attribute(15 + 1) - v.BgColor = gocui.Attribute(0) - - v.Frame = false - v.Editable = false - v.Wrap = false - v.Clear() - } - - v, err := g.SetView("out", -1, 1, maxX, maxY-4) - if err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Frame = false - v.Wrap = true - v.Editor = gocui.EditorFunc(editor) - v.Editable = true - SLTWriter = v - } - // For more information about KeepAutoscrolling, see Scrolling in editor.go - v.Autoscroll = true - g.SetViewOnTop("out") - //go func (){ - // for range time.Tick(time.Millisecond * 100) { - state.Render() - // } - // }() - go fetch_messages(g) - return nil -} - -func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit -} - -func prev(g *gocui.Gui, v *gocui.View) error { - state.PreviousScreen() - return state.Render() -} - -func next(g *gocui.Gui, v *gocui.View) error { - state.NextScreen() - return state.Render() -} - -func connect(command string) { - if len(command) > 1 && command[0] == '/' { - parts := strings.Split(command, " ") - switch parts[0] { - case "/win": - if len(parts) == 2 { - i, e := strconv.Atoi(parts[1]) - if e == nil { - state.GotoScreen(i) - } - } - case "/newgroup": - // TODO /newgroup [name] - case "/pm": - // TODO /pm [name] - case "/newfriend": - // TODO /newfriend [onion] - go cwtch_client.AddContactRequest(parts[1]) - case "/block": - // TODO /block [name] - case "/invite": - // TODO /invite [name]... - case "/accept": - // TODO /accept (a group inivite / a friend request) - case "/reject": - // TODO /invite (a group invite / a friend request) - case "/rejoin": - // TODO /rejoin group - case "/leave": - // TODO /leave (a group) - case "/close": - // TODO /close (a group) - default: - screen := state.GetScreen(0) - screen.AppendToLog("Error: Unknown Command " + parts[0]) - state.Render() - } - } -} diff --git a/model/group.go b/model/group.go index 07e1ac2..28c6b1e 100644 --- a/model/group.go +++ b/model/group.go @@ -64,15 +64,16 @@ func (g *Group) Invite() []byte { return invite } -func (g *Group) AddMessage(message *protocol.DecryptedGroupMessage, verified bool) { - timelineMessage := Message{ +func (g *Group) AddMessage(message *protocol.DecryptedGroupMessage, verified bool) *Message { + timelineMessage := &Message{ Message: message.GetText(), Timestamp: time.Unix(int64(message.GetTimestamp()), 0), Signature: message.GetSignature(), Verified: verified, PeerID: message.GetOnion(), } - g.Timeline = append(g.Timeline, timelineMessage) + g.Timeline = append(g.Timeline, *timelineMessage) + return timelineMessage } // AddMember ... diff --git a/model/profile.go b/model/profile.go index 2fa0e40..7b7b4c7 100644 --- a/model/profile.go +++ b/model/profile.go @@ -68,17 +68,6 @@ func (p *Profile) GetCwtchIdentityPacket() (message []byte) { return } -// GetCwtchIdentity returns the wire message for conveying this profiles identity. -func (p *Profile) GetCwtchIdentity() (message []byte) { - ci := &protocol.CwtchIdentity{ - Name: p.Name, - Ed25519PublicKey: p.Ed25519PublicKey, - } - message, err := proto.Marshal(ci) - utils.CheckError(err) - return -} - // AddCwtchIdentity takes a wire message and if it is a CwtchIdentity message adds the identity as a contact // otherwise returns an error func (p *Profile) AddCwtchIdentity(onion string, ci *protocol.CwtchIdentity) { @@ -90,15 +79,14 @@ func (p *Profile) AddContact(onion string, profile PublicProfile) { p.Contacts[onion] = profile } -// VerifyMessage confirms the authenticity of a message given an onion, message and signature. -func (p *Profile) VerifyMessage(onion string, message string, signature []byte) bool { - return ed25519.Verify(p.Contacts[onion].Ed25519PublicKey, []byte(message), signature) -} - // VerifyMessage confirms the authenticity of a message given an onion, message and signature. func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, timestamp int32, signature []byte) bool { - m := message + groupID + strconv.Itoa(int(timestamp)) - return ed25519.Verify(p.Contacts[onion].Ed25519PublicKey, []byte(m), signature) + contact, found := p.Contacts[onion] + if found { + m := message + groupID + strconv.Itoa(int(timestamp)) + return ed25519.Verify(contact.Ed25519PublicKey, []byte(m), signature) + } + return false } // SignMessage takes a given message and returns an Ed21159 signature @@ -135,7 +123,7 @@ func (p *Profile) ProcessInvite(gci *protocol.GroupChatInvite, peerHostname stri p.AddGroup(group) } -// AddGroup is a conveniance method for adding a group to a profile. +// AddGroup is a convenience method for adding a group to a profile. func (p *Profile) AddGroup(group *Group) { existingGroup, exists := p.Groups[group.GroupID] if !exists { @@ -154,24 +142,24 @@ func (p *Profile) AddGroup(group *Group) { } // AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups. -func (p *Profile) AttemptDecryption(ciphertext []byte) { +func (p *Profile) AttemptDecryption(ciphertext []byte) (bool, *Message) { for _, group := range p.Groups { success, dgm := group.DecryptMessage(ciphertext) log.Printf("Decrypt Attempt %v %v", success, dgm) if success { - // FIXME verified := p.VerifyGroupMessage(dgm.GetOnion(), group.GroupID, dgm.GetText(), dgm.GetTimestamp(), dgm.GetSignature()) - group.AddMessage(dgm, verified) + return true, group.AddMessage(dgm, verified) } } + return false, nil } // EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and // profile -func (p *Profile) EncryptMessageToGroup(message string, groupID string) (ciphertext []byte, signature []byte) { +func (p *Profile) EncryptMessageToGroup(message string, groupID string) (ciphertext []byte) { group := p.Groups[groupID] timestamp := time.Now().Unix() - signature = p.SignMessage(message + groupID + strconv.Itoa(int(timestamp))) + signature := p.SignMessage(message + groupID + strconv.Itoa(int(timestamp))) dm := &protocol.DecryptedGroupMessage{ Onion: proto.String(p.Onion), Text: proto.String(message), diff --git a/model/profile_test b/model/profile_test index 38c43bd..0fba87b 100644 --- a/model/profile_test +++ b/model/profile_test @@ -1 +1 @@ -{"Name":"Sarah","Ed25519PublicKey":"57Vq0cuSR354/iWji5HYo1wRLejxtUHt3oG7Su/apTs=","Trusted":false,"Blocked":false,"Contacts":{},"Ed25519PrivateKey":"J8ccHg0nDRTt2hUcWFgcja7zp3q+stire73hfHQDlGDntWrRy5JHfnj+JaOLkdijXBEt6PG1Qe3egbtK79qlOw==","OnionPrivateKey":{"N":106324796443231372795329965451792784374908475257731042677987977989945962496419819543299936408371285042498791020833780363910141715665590232533252599354509160780458961503863367842703504977430864504358617442294691508603803037645994752383061089701523494391193738887906210739432038880652477581768063943643214298049,"E":65537,"D":52882540623824249319263554234503221073355763301661673056925036705376788585582196893867658378736750343245656531655368795367730890395281737333932003731626103306294071690361237344309695692052986927494790956908776032726936870574424804180862906949851814015510618834696423905205181829311680766288721499332239092113,"Primes":[10562771257917076828400600569279483678021278216385409474099349073602122852652613259317666843104807170827922546052248527778489213047126675685744594423055011,10065994410656021893110220970838146088057898596082565348257922502730196228417367015775815130802667391440031211811041050535960601302390904626913561708368459],"Precomputed":{"Dp":14666710170902757089650955213153379231578136284710503412469914181268492295823547104657028590300707272919739257072411249032493376066779490782348262698903,"Dq":8351748051846008307669886865591879879827216596130210009260002655117828861809706712999156561217721929245207091771627754763620453429647494239789002112611857,"Qinv":5157808396374745421942830050538059633959311365944586097995200192447276074688521466971512517666145546534306106545362826255395595072097617674779280863162908,"CRTValues":[]}},"Onion":"cf3nb4kb4ekfep7t","Groups":{}} \ No newline at end of file +{"Name":"Sarah","Ed25519PublicKey":"UsCWNjTraCR3Z2dqtsW1A++ubJD5E5FnC+ACSIP/8hk=","Trusted":false,"Blocked":false,"Contacts":{},"Ed25519PrivateKey":"n89RBVJD3T2KAxOUFsBgJcyBcOtgu0eeGs464orKNT9SwJY2NOtoJHdnZ2q2xbUD765skPkTkWcL4AJIg//yGQ==","OnionPrivateKey":{"N":123805553263348528457396531577890431199849926799850215350792942327090732394534595405543245927983354464965037398489749312360344572108681888509184005620780768892052483022600381118025061152186760477945751128712525497823295804735134482165349819329437248988124357774675993218222587847682647131583478848460913040243,"E":65537,"D":36374504906934646313489635099749458363262131933581273121740667172866198517734465027903858894110494685794311535589363611540022197170341482875998871297559138745889655544439413518334368519721005053246504047688085489173938252813690494094575739926433903954150053038634347562274030322729843548978331756633920952393,"Primes":[11696469543379902568303131723781955090117870500969155291133497643805055498454292242391505499241962759128566231590491957401660779652639301001660955427807509,10584865185531250093066719901396357065936568829189765800518363959386090914453659853194245194887656726848216858880202829552571699371710179494672080022887527],"Precomputed":{"Dp":10160546066712868045776669547990150376665097357075773683255583172245687391587977961328574691812932875168964159645365177332406882316926439191991697109426877,"Dq":9841112737610664672944159437140589723997848725150538012987853468625559479370285520771957035184623948003655181733807144213151331097197684620433635655501209,"Qinv":1336220763953049451525874385731316715417274408983287245077551897537966825948439529869489565316701741855547495791046015721525313869844240508900847744658372,"CRTValues":[]}},"Onion":"ln6hn7x65lfagspl","Groups":{}} \ No newline at end of file diff --git a/model/profile_test.go b/model/profile_test.go index e748865..1231e8b 100644 --- a/model/profile_test.go +++ b/model/profile_test.go @@ -22,14 +22,14 @@ func TestProfileIdentity(t *testing.T) { sarah := GenerateNewProfile("Sarah") alice := GenerateNewProfile("Alice") - message := sarah.GetCwtchIdentity() + message := sarah.GetCwtchIdentityPacket() - ci := &protocol.CwtchIdentity{} + ci := &protocol.CwtchPeerPacket{} err := proto.Unmarshal(message, ci) if err != nil { t.Errorf("alice should have added sarah as a contact %v", err) } - alice.AddCwtchIdentity("sarah.onion", ci) + alice.AddCwtchIdentity("sarah.onion", ci.GetCwtchIdentify()) if alice.Contacts["sarah.onion"].Name != "Sarah" { t.Errorf("alice should have added sarah as a contact %v", alice.Contacts) } @@ -49,15 +49,28 @@ func TestProfileGroup(t *testing.T) { sarah.ProcessInvite(gci.GetGroupChatInvite(), alice.Onion) group := alice.GetGroupByGroupId(gid) - c, s := sarah.EncryptMessageToGroup("Hello World", group.GroupID) - alice.AttemptDecryption(c, s) + c := sarah.EncryptMessageToGroup("Hello World", group.GroupID) + alice.AttemptDecryption(c) gid2, invite2 := alice.StartGroup("bbb.onion") gci2 := &protocol.CwtchPeerPacket{} proto.Unmarshal(invite2, gci2) sarah.ProcessInvite(gci2.GetGroupChatInvite(), alice.Onion) group2 := alice.GetGroupByGroupId(gid2) - c2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID) - alice.AttemptDecryption(c2, s) + c2 := sarah.EncryptMessageToGroup("Hello World", group2.GroupID) + alice.AttemptDecryption(c2) + bob := GenerateNewProfile("bob") + bob.ProcessInvite(gci2.GetGroupChatInvite(), alice.Onion) + c3 := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID) + ok, message := alice.AttemptDecryption(c3) + if ok != true || message.Verified == true { + t.Errorf("Bobs message to the group should be decrypted but not verified by alice instead %v %v", message, ok) + } + + eve := GenerateNewProfile("eve") + ok, _ = eve.AttemptDecryption(c3) + if ok { + t.Errorf("Eves hould not be able to decrypt messages!") + } } diff --git a/papers/metadata-resistant-group-chat.md b/papers/metadata-resistant-group-chat.md deleted file mode 100644 index 6c6d656..0000000 --- a/papers/metadata-resistant-group-chat.md +++ /dev/null @@ -1,314 +0,0 @@ -# Metadata-Resistant Group Chat - -# Introduction - -# Use Cases - -* Harm Reduction Forums (TODO: Rasmus probably wrote a paper on this) - -- Long lived pseudonym linked to long term reputation building, fundamentally anonymous due to underlying anonymity network. -- Repudiation not a hard requirement, in many cases actively harmful - the ability to state that an expert gave a certain piece of advice is vital in both building reputation and for the safety of the participants. -- Relies on a trusted server to store messages - -* Public Ricochet identities in Private Group Chat - -- Repudiation is a more useful to protect against compromise of the group. //XXX Does the benefit of repudiation outweigh the inefficiency of implementing SSA in this way? - -* *Confidentiality*: Only the intended recipients are able to read a message. Specifically, the message must not be readable by a server operator that is not a conversation participant. -* *Integrity*: No honest party will accept a message that has been modified in transit. -* *Authentication*: Each participant in the conversation receives proof of possession of a known long-term secret from all other participants that they believe to be participating in the conversation. In addition, each participant is able to verify that a message was sent from the claimed source. -* *Participant Consistency*: At any point when a message is accepted by an honest party, all honest parties are guaranteed to have the same view of the participant list. -* *Destination Validation*: When a message is accepted by an honest party, they can verify that they were included in the set of intended recipients for the message. -* *Forward Secrecy*: Compromising all key material does not enable decryption of previously encrypted data. -* *Backward Secrecy*: Compromising all key material does not enable decryption of succeeding encrypted data. -* *Anonymity Preserving*: Any anonymity features provided by the underlying transport privacy architecture are not undermined (e.g., if the transport privacy system provides anonymity, the conversation security level does not deanonymize users by linking key identifiers). - -* *Speaker Consistency*: All participants agree on the sequence of messages sent by each participant. A protocol might perform consistency checks on blocks of messages during the protocol, or after every message is sent. -* *Causality Preserving*: Implementations can avoid displaying a message before messages that causally precede it. -* *Global Transcript*: All participants see all messages in the same order. - -* *Computational Equality* - All chat participants share equal computational load -* *Trust Equality* - No participant is more trusted than any other -* *Subgroup messaging*: Messages can be sent to a subset of participants without forming a new conversation. -* *Contractible Membership*: After the conversation begins, participants can leave without restarting the protocol. -* *Expandable Membership*: After the conversation begins, participants can join without restarting the protocol - - - -In addition we require the following usability criteria - -* *Out-of-Order Resilient*: If a message is delayed in transit, but eventually arrives, its contents are accessible upon arrival. -* *Dropped Message Resilient*: Messages can be decrypted without receipt of all previous messages. This is desirable for asynchronous and unreliable network services. -* *Asynchronous*: Messages can be sent securely to disconnected recipients and received upon their next connection. -* *Multi-Device Support*: A user can participate in the conversation using multiple devices at once. Each device must be able to send and receive messages. Ideally, all devices have identical views of the conversation. The devices might use a synchronized long-term key or distinct keys. - - -*Privacy Preserving*: The approach leaks no conversation metadata to other participants or even service operators. - - -*Sender Anonymity*: When a chat message is received, no non-global entities except for the sender can determine which entity produced the message. -*Recipient Anonymity*: No non-global entities except the receiver of a chat message know which entity received it. -*Participation Anonymity*: No non-global entities except the conversation participants can discover which set of network nodes are engaged in a conversation. -*Unlinkability*: No non-global entities except the conversation participants can discover that two protocol messages belong to the same conversation - -# Metadata-Resistance - - - - -## Ricochet: An Overview - -Ricochet is a secure messaging protocol which, through it's use of the Tor hidden service protocol provides online 2-party instant messaging with *sender anonymity*, *recipient anonymity*, *participation anonymity* and partial *unlikability* (to network adversaries with limited scope). - -Ricochet is *Anonymity Preserving* and provides a number of other properties including *Confidentiality*, *Integrity* , *Authentication*, *Speaker Consistency* and *Causality Preservation* for 2-way instant messaging. - -In this paper we will build upon the ricochet protocol to define and implement a metadata-resistant group chat protocol. - -To start it is important to understand the properties that Ricochet cannot give us at all, as well as properties which Ricochet does provide for 2-party exchanges but which cannot extend to multi-party protocols. - -Ricochet is not *Asynchronous* it requires both parties to be online at the same time in order to exchange messages. - -Further properties like *Forward Secrecy*, *Participation Anonymity*, *Authentication* are derived from the hidden service connection between two servers, and thus cannot be trivially extended to a group setting, and must instead be reinforced at a higher level. - -The the next section we will demonstrate how these properties can be achieved. - -## Online Group Chat - -A naive implementation of metadata-resistant group chat which supports the above threat model is a scheme we will call, Online Group Chat (*ORC*). - -The protocol for ORC is as follows: - -* Setup: Each client involved in the group chat establishes a ricochet channel with every other client. -* Messaging: When a client wishes to send a message they must first encrypt the message to every participating client, then send these messages to every client along with a signature. -* Message Receipt / Attestation: Once a client has received a message, they must decrypt it and compare the contents of the message with the hash, if they match, they must then check with all the other clients to ensure that they all received the same hash. -* Teardown: The group chat ends when the clients destroy their ricochet channels. Offline clients are unable to participate in the chat from then on. - -ORC requires every client involved in the group chat to maintain a connection to every other client, this requires `n!/2`s communication channels. As such, like Ricochet, this scheme is not *Asynchronous* - -However this approach does provide many of our desirable properties including *Authentication*, *Anonymity Preserving*, *Causality Preserving*, *Computational Equality* and *Trust Equality* - -It is clear that a pure peer-2-peer solution based on hidden services cannot account for all of our desired properties. We will now show how a hybrid solution features peer-to-peer channels as well as relay-server can be used to achieve all of our desired properties. - -## Introducing a Server - -In order to attain the property of *Asynchronous* communication, we must introduce long-lived infrastructure. [citation?] - -As stated above it is essential that this infrastructure *not* be trusted, and that is must gain no knowledge about the participants in a particular group conversation. - -For simplicity we will separate the concept of *Asynchronous Key-Exchange* and *Asynchronous Conversation* and first demonstrate how *Asynchronous Conversation* can be attained, if we assume *Online Key-Exchange*. - -If it can be assumed that every participant in the group performs some kind of key exchange operation with every other participant prior to the conversation starting, and then all future communication is mediated through sending and receiving encrypted packets to the server then we can achieve all of our desired properties. - -This is of course a very hand-wavy assumption, and we will return later in this paper to more formally defining a key-exchange mechanism which fully satisfies this property in a meaningful way. - -The originator of the group chat, Alice sends a group-key `GI` and a group-chat-server identifier `S` to participants Bob and Carol. - -Alice, Bob and Carol all create ricochet connections to `S` using ephemeral ricochet IDs. - -When any of Alice, Bob and Carol which to send a message, they sign it using long term signing keys, and encrypt it using the group key and send the resulting ciphertext to `S` where it can be downloaded by the others. - -`S` has an idea of the number of ephemeral connections to a given group, and thus can derive the number of participants - but gains no information as to who is speaking, what is being said. - -If each message includes a hash of the previous seen message identifiers, then `S` has no ability to modify the transcript (by e.g. not distributing a message to the rest of the group) without being detected. Because each connection to `S` is ephemeral and is regularly torn-down and rebuild `S` gains no information useful to target modification. - -# Key Exchange - -Pair-wise key exchange can be done when each client is online by establishing a ricochet connection and then simply transmitting public keys for encryption and signing. - -Session keys for the groups can then be pairwise encrypted and uploaded to the server. - -To expand the group the initiator invites a new user by generating a new session key, and pairwise encrypting that to all users. - -When the group contracts, the same thing happens. To avoid rollover vulnerability i.e. the time between a user leaving the group and the initiator sending out new keys, session keys can be generated before hand and send pairwise to each member (minus the one who may leave). - -# Public Channels - -As discussed in an above section on use cases, there are occasionally instances where open-group chat channels are desired, but where we still wish to retain anonymity, authentication and transcript consistency. - -In this case many properties such as forward & backward secrecy -are not applicable - - -# Untrusted Server - -IN cwtch we assume all supporting infrastructure is untrusted,even in cases where i may be setup by one of the chat participants. - -Cwtch servers may be used by multiple groups or just one. - -Cwtch servers should never learn the identity of participants within a group, the content of any communication, any group session keys. - -All participants within a cwtch session must be able to detect and/or successfully mitigate when a cwtch server is acting dishonestly. Dishonest behavior is defined as: - -* Failing to relay any message - this will be detected when a message id appears in subsequent messages, but which is not known to some participants. In this case, participants will request a message is resent. -* Modifying a relayed message - this will be detected as a failure to decrypt -* Attaching duplicate messages to the timeline - duplicate messages will be ignored. - - -Private message are just group messages that only include a subset of the participants - these require an additional group setup and are indistinguishable from simply setting up a new group. - - - -# Types of Messages - -* Group Public Messages - All general chat messages. -* Group Administration Messages - Requesting a message be resent. - -## Supporting Multiple Devices - -Multiple devices can be supported by simply including multiple people within the group setting and having these devices interact with the group as regular participants. - - -### PROBLEMS - -This is METADATA, how to resolve getting the same - -Alice(a) -> Carol -Alice(a) -> Alice(b) - -Carol -> Alice(a) -Carol -> Alice(b) - -If only Alice has a shared device this breaks private message indistinguishably group property. - -The *only* way to fix this (without introducing random delays) is to have people sent multiple encrypted messages per private message. - -Number of messages to send are max(keys[user]) - -THIS IS UGLY - - -## - -Peer Channels -Server Channels - -## SPAM - -Group bucketting on the server - -Buckets prevent spam flooding, but also expose group metadata to the server. - -To not have bucketing means clients attempt to parse all valid messages on the server...which doesn't scale...and prevents us from being able to detect a malicious server (if the majority of messages are always unable to be decrypted by clients of a server then we have no mechanism for detecting bad behaving servers) - -- Overloading the server with too many messages -- DDoS the server such that clients cannot send messages - -Private servers will always have some insight into group activity - -Public servers are vulnerable to spam floods. - -Metadata Protection vs Resistance - - A --->S - ^ ^ - . | - . . > B - -# Cascade Design - -To keep the design simple we propose that all clients of a server receive every message from a server. - -This makes it impossible for a server to associate relationships between the message senders, as everyone receieves a copy of the timeline ( this is equivilant to a naive PIR design) - -The reason we can get away with such a simple design is that, by design, Cwtch has no central servers. Each group can choose any random cwtch server to act as a relay for the messages for that group. - -We can even Cascade servers by having a server act as a client to another server. This allows us to scale reading resources. - -One major potential pitfall with this kind of design is spam. While someone counteracted by the decentralized nature of the protocl (anyone can setup and user a cwtch server), we must consider how to prevent an individual cwtch server from being overwhelmed by bogus messages. - -Proof-of-Work places a cap on the number of messages that are accepted by the server that is proportional to the computational power of an adversary. - -This certainly doesn't prevent a moderately funded adversary from overwhelming the system, but combined with the ability of groups to select and move to arbitrary servers, it makes targetted attacks on the communication of particular groups difficult. - -# Signature Security - -In order to preserve metadata resistant, the system requires that a participant requires access to the group and access to the peer identity in order to verify a message came from a given peer. - -As such it is not sufficient to sign just the message, doing so would allow someone to spoof a message from a given peer by first obtaining a message in one group and then broadcasting it to another. - -As such signatures in cwtch have the following structure - -Sign(Message . Ciphertext) - -The ciphertext is encrypted - - -# Lit Reviews - -OnionPIR, requires registration of users, makes no attempt to solve challenges of malicious servers. Recommends blinded signatures as an approach to solve spam. - -Time Epoch based systems. - -Demmler, Daniel, Marco Holz, and Thomas Schneider. "OnionPIR: Effective Protection of Sensitive Metadata in Online Communication Networks." International Conference on Applied Cryptography and Network Security. Springer, Cham, 2017. - -Corrigan-Gibbs, Henry, Dan Boneh, and David Mazières. "Riposte: An anonymous messaging system handling millions of users." Security and Privacy (SP), 2015 IEEE Symposium on. IEEE, 2015. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/papers/metadata-resistant-protocols.md b/papers/metadata-resistant-protocols.md deleted file mode 100644 index f5e52f9..0000000 --- a/papers/metadata-resistant-protocols.md +++ /dev/null @@ -1 +0,0 @@ -# Metdata-Resistant Protocols: An Overview diff --git a/peer/client.go b/peer/client.go deleted file mode 100644 index 3abe7c6..0000000 --- a/peer/client.go +++ /dev/null @@ -1,83 +0,0 @@ -package peer - -import ( - "crypto/rand" - "crypto/rsa" - "github.com/s-rah/go-ricochet" - "github.com/s-rah/go-ricochet/channels" - "github.com/s-rah/go-ricochet/connection" - "github.com/s-rah/go-ricochet/identity" - "time" -) - -type CwtchClient struct { - connection.AutoConnectionHandler - In chan string - Out chan string -} - -func (cc *CwtchClient) JoinServer(hostname string) { - pk, _ := rsa.GenerateKey(rand.Reader, 1024) - cc.Init() - - cc.RegisterChannelHandler("im.ricochet.chat", func() channels.Handler { - chat := new(channels.ChatChannel) - chat.Handler = cc - return chat - }) - - rc, err := goricochet.Open(hostname) - - if err == nil { - _, err := connection.HandleOutboundConnection(rc).ProcessAuthAsClient(identity.Initialize("", pk)) - if err == nil { - go func() { - - rc.Do(func() error { - rc.EnableFeatures([]string{"im.ricochet.chat"}) - rc.RequestOpenChannel("im.ricochet.chat", &channels.ChatChannel{ - Handler: cc, - }) - return nil - }) - - sendMessage := func(message string) { - rc.Do(func() error { - channel := rc.Channel("im.ricochet.chat", channels.Outbound) - if channel != nil { - chatchannel, ok := channel.Handler.(*channels.ChatChannel) - if ok { - chatchannel.SendMessage(message) - } - } else { - //XXX: FIXME - } - return nil - }) - } - - for { - message := <-cc.In - sendMessage(message) - } - - }() - rc.Process(cc) - } - } -} - -// OnClosed ... -func (cc *CwtchClient) OnClosed(err error) { -} - -// ChatMessage passes the response to messages. -func (cc *CwtchClient) ChatMessage(messageID uint32, when time.Time, message string) bool { - cc.Out <- message - return true -} - -// ChatMessageAck does nothing. -func (cc *CwtchClient) ChatMessageAck(messageID uint32, accepted bool) { - -} diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 91fe8d5..f23ed9e 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -83,7 +83,7 @@ func (cp *CwtchPeer) JoinServer(onion string) { func (cp *CwtchPeer) SendMessageToGroup(groupid string, message string) { group := cp.Profile.GetGroupByGroupId(groupid) psc := cp.connectionsManager.GetPeerServerConnectionForOnion(group.GroupServer) - ct, _ := cp.Profile.EncryptMessageToGroup(message, groupid) + ct := cp.Profile.EncryptMessageToGroup(message, groupid) gm := &protocol.GroupMessage{ Ciphertext: ct, } diff --git a/peer/send/peer_send_channel_test.go b/peer/send/peer_send_channel_test.go index 4bc3691..069e66d 100644 --- a/peer/send/peer_send_channel_test.go +++ b/peer/send/peer_send_channel_test.go @@ -67,6 +67,11 @@ func TestPeerSendChannel(t *testing.T) { var sg spam.Guard sg.Difficulty = 2 + closed := false + channel.CloseChannel = func() { + closed = true + } + channel.SendMessage = func(message []byte) { packet := new(protocol.CwtchServerPacket) proto.Unmarshal(message[:], packet) @@ -94,6 +99,10 @@ func TestPeerSendChannel(t *testing.T) { t.Errorf("send channel should have successfully sent a valid group message") } + if !closed { + t.Errorf("send channel should have successfully closed after a valid group message") + } + pfc.Closed(nil) } diff --git a/peer/test_profile b/peer/test_profile index 96ec39e..8ff825d 100644 --- a/peer/test_profile +++ b/peer/test_profile @@ -1 +1 @@ -{"Profile":{"Name":"alice","Ed25519PublicKey":"woBpoPixOQlewrOj55rvUUJXO6SYjSbds+x5wBSD/nE=","Trusted":false,"Blocked":false,"Contacts":{},"Ed25519PrivateKey":"zF81FX4HdjfH8y9GEkkMuP3grW+6YHLUq5xt2BGdu93CgGmg+LE5CV7Cs6Pnmu9RQlc7pJiNJt2z7HnAFIP+cQ==","OnionPrivateKey":{"N":139926795769138065515184049224038533094758142244011440346432394711998122006646506685316823692319561121294993561288095105636848170048849868725177766607476321666954798718648650798310074246526178993249155964685028684884855868827006247404286011381915601081225045627232142906774434198218068995980294397037791794267,"E":65537,"D":80563006923674971788217949087853364805598501324340199865601622742387126930997644639776915458173154092952438958879467974136673817850271634292188117664829075982704832540445727518368206591976958327683396293490831042373962322741974344132577591789404120121585476701598634477623181079921183580001754668835068766913,"Primes":[11996718101524297693400014654326964581090960285071398726861380106816214227409962837267266227138498003607972122710998689259606190329862913588172576335353147,11663756252750410861345204036901556299677356139785595193403181361865123292907581200748114782681955962873888839331259846486482553416702234941039306713860961],"Precomputed":{"Dp":3630482172017812779852640350325261890791110599109221522954083214954696992114710666516955171625766069633289761657189633399236607913272978091676865075591787,"Dq":476609232111106707458114598025884123022658341735902222072016108260597832955528975320863808047702489716896933209166026349860388453086479167067507871579713,"Qinv":8004940118786479816457227792582435507151418905449158436675099080630617341053710180416725805457646331045119350055846124889161926521155923764007407649644659,"CRTValues":[]}},"Groups":{}}} \ No newline at end of file +{"Profile":{"Name":"alice","Ed25519PublicKey":"GQjDT/ADqudCIZq/7i4flbRaLzBnPCj2IYI8S1qIWGM=","Trusted":false,"Blocked":false,"Contacts":{},"Ed25519PrivateKey":"hbG3RyQQ+r1PpW1UnE8B5kf54zDfHOvBz4bMgdW2IsAZCMNP8AOq50Ihmr/uLh+VtFovMGc8KPYhgjxLWohYYw==","OnionPrivateKey":{"N":118949437147999046779871097106577144824161312908242780099796773587901402109754265146160871793580686203857212353483161141299741736678609148375472482302726670540249479081752805338231525747022348201429435662507924057367176907320212979720249273484193414490356905146774628953953268592916404780858351871034973364651,"E":65537,"D":94181992508763271685525597753290425592727080704359767797710520442647537373960641663479161362904853560650542536805082850652072851335729546948313816304847468284377523922190377615541996345142801491115074550379060687848886736978676340143503908396698667066436989395508107231468852742294096428057812643803322726345,"Primes":[11093306403644218894981011451014980780451441495773111118759223267959318051098530285527407597744590710343913726393148960031173198769212333898347445271695669,10722631541929052848558646606992517878067322185889508722580532907231300392570442727893142668003376876119846036715047467381256380482736518424207582333117279],"Precomputed":{"Dp":288432398672654363139106817714108476889226792633129092670792322636109037018354450257697217549731946387933976071134257410212843595262795321158796507972129,"Dq":2883332097340673182326762426644935571740855072431310743824659832219009213395012164024304640710186775224072610968297015695864644296309957187387433410997235,"Qinv":9556280274383976646018218885445548987752711277953338363729408513927401398507573496665859225463898071345877217574624168387272500232882404725411489440703861,"CRTValues":[]}},"Onion":"kuzx57bbs6q7nymu","Groups":{}}} \ No newline at end of file diff --git a/protocol.md b/protocol.md deleted file mode 100644 index 09b3ac2..0000000 --- a/protocol.md +++ /dev/null @@ -1,141 +0,0 @@ -Title: Cwtch Protocol: Facilitating Anonymous Collaboration -Author: Sarah Jamie Lewis -Doc Class : [11pt, twocolumn]article - -# Actors & Identity - -All parties in a Cwtch System run ricochet nodes. - -## Cwtch Client - -A Cwtch Client identity consists of: - -* A Name -* An Onion Address (and an associated private key) -* A Nacl Public Key (and an associated private key) - -## Cwtch Server - -A Cwtch Server identity consists of: - -* A Name -* An Onion Address (and an associated private key) - -# Threat Model - -## Impersonation - -One of the key attacks we must defend against in a multiple-party messaging -system is the risk of one party attempting to impersonate another. - -There are two levels to this kind of the attack, systemic impersonation and superficial -impersonation. - -Systemic Impersonation would result from the system being unable to distinguish between -two parties. Due to the anonymous nature of our approach, we adopt a long lived public key -that can be used to link & verify that seperate messages belong to the same party. - -Superficial Impersonation is more tricky, it would result from a user being tricked to accepting -that two distinct identities are the same. We cannot rely on the user to compare public keys to avoid -this. An attacker may try and change their profile name to match a target - it should be noted that restricting profile names to be unique per Cwtch system is not sufficient to prevent this attack (because of homoglyph style attacks), nor is such a restriction desirable. - -To prevent superficial style impersonation we take a multi step approach: - -// TODO - -# Client <-> Server Messages - -When a client wishes to join a Cwtch system, they must first identify the address of the Cwtch server. - -One a Cwtch server is known, the client connects to the Cwtch server over Ricochet, and performs a standard `im.ricochet.auth.hidden-service` authentication. - -At this point the Cwtch Server initiates an `EnableFeatures` setup in an attempt to detect that the client is indeed a Cwtch client, or if it is a bare bones Ricochet client. - - message ServerIdentify { - string name = 1; - string topic = 2; - int32 difficulty = 3; - } - -If a Cwtch Client is detected, that the CwtchServer sets up a `im.cwtch.event` through which all other communication between the client and the server takes place. - - enum EventType { - NIL = 0; - JOIN = 1; - LEAVE = 2; - MESSAGE = 3; - } - - message Event { - EventType type = 1; - string client = 2; - int64 timestamp = 3; - string detail = 4; - bytes proof = 5; - } - -## Event Types - -* *JOIN* events are sent when a client connects to the server -* *LEAVE* events are sent when a client disconnects from the server -* *MESSAGE* events are sent when a client sends a message to the server, these are broadcast to every other connected client. - -## Sending Messages - - message Message { - string message = 1; - bytes signature = 2; - bytes spamguard = 3; - } - -## Message Signatures - - - -### Preventing Spam Through SpamGuard - -To prevent abusive clients from flooding the server (and therefore other clients) with -a large number of message, all messages must include a complet `spamguard` field. - -This field is `nonce|sha256(message.signature.nonce)`. Valid spamguard fields must -ensure that the sha256 digest begins with `difficulty` number of `0x00` as defined -by the server profile. - -This requires that clients try a number of random nonces prior to sending any messages. - -Message packaets with invalid SpamGuard digests will be discarded by the server. - -To ensure that a client cannot find a single valid SpamGuard digest, e.g. for the message `hi`, -and use that to spam clients. - - -# Client <-> Client Messages - -## ClientIdentify - -When 2 clients are online at the same time, they can use the opporunity to exchange -`ClientIdentify` messages over an `im.cwtch.client.identify` channel: - - message ClientIdentify { - string name = 1; - bytes ed25519_public_key = 2; - bytes nacl_public_key = 3; - } - -* `name` is a readable name to help identify this client. -* `ed25519_public_key `is used to authenticate public messages from this client. -* `nacl_public_key` is used to authenticate private messages from this client. - -Once a client has the Profile of another client, they can proceed to validate that -the messages they have received from the server via the `im.cwtch.event` that are tagged -as originating from the client do in fact validate. - -## Private Messages - -When both clients are online, private messages can be sent to eachother using an -`im.ricochet.chat` channel. - -When either client is offline, private messages are sent utilizing the `im.cwtch.message` channel - instead of a regular message being sent, the message is begins `ENC-` and contains an encrypted message. - -Note that there is no metadata associated with private messages, and as such each client must attempt to decrypt the message with their own keys, and discard messages that fail. - diff --git a/server/ms.test b/server/ms.test index 63f8b40..afe6366 100644 --- a/server/ms.test +++ b/server/ms.test @@ -1 +1,2 @@ {"ciphertext":"SGVsbG8gdGhpcyBpcyBhIGZhaXJseSBhdmVyYWdlIGxlbmd0aCBtZXNzYWdlIHRoYXQgd2UgYXJlIHdyaXRpbmcgaGVyZS4="} +{"ciphertext":"SGVsbG8gdGhpcyBpcyBhIGZhaXJseSBhdmVyYWdlIGxlbmd0aCBtZXNzYWdlIHRoYXQgd2UgYXJlIHdyaXRpbmcgaGVyZS4="} diff --git a/peer/cwtch_peer_server_intergration_test.go b/testing/cwtch_peer_server_intergration_test.go similarity index 85% rename from peer/cwtch_peer_server_intergration_test.go rename to testing/cwtch_peer_server_intergration_test.go index 36ab455..1c35101 100644 --- a/peer/cwtch_peer_server_intergration_test.go +++ b/testing/cwtch_peer_server_intergration_test.go @@ -1,12 +1,13 @@ -package peer +package testing import ( "testing" "time" + "git.mascherari.press/cwtch/peer" ) func TestCwtchPeerIntegration(t *testing.T) { - alice := NewCwtchPeer("Alice") + alice := peer.NewCwtchPeer("Alice") id, _ := alice.Profile.StartGroup("ylhbhtypevo4ympq") alice.Profile.AddContact(alice.Profile.Onion, alice.Profile.PublicProfile) alice.JoinServer("ylhbhtypevo4ympq") diff --git a/todo.md b/todo.md deleted file mode 100644 index 598d8fd..0000000 --- a/todo.md +++ /dev/null @@ -1,18 +0,0 @@ -# Protocol Work -[ ] Cwtch Paper Plan -[X] Message channel with Spam guard -[ ] Profile Exchange Channel -[ ] Download all messages from server. -[ ] Server timeout -[ ] Server profile update -[ ] Complete threat model/protocol doc -[ ] Offline private messaging - -# UI Work -[ ] Scroll back in the UX -[ ] Set Profile Name - - -Offline Anonymous Group Chat -- Define Threat Model -- Contrast with Other Approaches (SoK paper) diff --git a/ui/action.go b/ui/action.go deleted file mode 100644 index b21e872..0000000 --- a/ui/action.go +++ /dev/null @@ -1,15 +0,0 @@ -package ui - -type ActionType int - -const ( - NONE ActionType = iota - OPEN - SEND -) - -type Action struct { - Type ActionType - ID string - Context string -} diff --git a/ui/chat_screen.go b/ui/chat_screen.go deleted file mode 100644 index 717a0ee..0000000 --- a/ui/chat_screen.go +++ /dev/null @@ -1,33 +0,0 @@ -package ui - -type ChatScreen struct { - Title string - Elements []Element - Position int - GroupID string - MessageBoxText string -} - -func (cs *ChatScreen) Title() string { - return cs.Title -} - -func (cs *ChatScreen) MoveUp() { - if cs.Position > 0 { - cs.Position -= 1 - } -} - -func (cs *ChatScreen) MoveDown() { - if cs.Position < len(cs.Elements)-1 { - cs.Position += 1 - } -} - -func (cs *ChatScreen) Transition() { - -} - -func (cs *ChatScreen) Action() Action { - return Action{SEND, cs.GroupID, cs.MessageBoxText} -} diff --git a/ui/contact_screen.go b/ui/contact_screen.go deleted file mode 100644 index aaf863e..0000000 --- a/ui/contact_screen.go +++ /dev/null @@ -1,37 +0,0 @@ -package ui - -type ContactScreen struct { - Title string - Elements []Element - Position int - InSearch bool -} - -func (cs *ContactScreen) Title() string { - return cs.Title -} - -func (cs *ContactScreen) MoveUp() { - if cs.Position > 0 { - cs.Position -= 1 - } -} - -func (cs *ContactScreen) MoveDown() { - if cs.Position < len(cs.Elements)-1 { - cs.Position += 1 - } -} - -func (cs *ContactScreen) Transition() { - cs.InSearch = !cs.InSearch -} - -func (cs *ContactScreen) Action() Action { - if cs.InSearch { - return Action{NONE, "", ""} - } else { - return Action{OPEN, cs.Elements[cs.Position].ID, ""} - } - return Action{NONE, "", ""} -} diff --git a/ui/contact_screen_test.go b/ui/contact_screen_test.go deleted file mode 100644 index cc81cad..0000000 --- a/ui/contact_screen_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package ui - -import ( - "testing" -) - -func TestBasicOperations(t *testing.T) { - screen := new(ContactScreen) - screen.Elements = []Element{{"alice", "1"}, {"bob", "2"}, {"carol", "3"}} - - screen.MoveDown() - screen.MoveDown() - action := screen.Action() - if action.Type != OPEN || action.ID != "carol" { - t.Errorf("action sequence should have opened carol instead %v", action) - } - - screen.MoveUp() - action = screen.Action() - if action.Type != OPEN || action.ID != "bob" { - t.Errorf("action sequence should have opened bob instead %v", action) - } - - screen.Transition() - action = screen.Action() - if action.Type != NONE { - t.Errorf("action sequence should done nothing instead %v", action) - } - -} diff --git a/ui/layout.go b/ui/layout.go deleted file mode 100644 index 2d18acc..0000000 --- a/ui/layout.go +++ /dev/null @@ -1,127 +0,0 @@ -package ui - -import ( - "fmt" - "github.com/fatih/color" - "github.com/jroimartin/gocui" -) - -type Element struct { - ID string - Contents string -} - -type Layout struct { - Title string - Elements []Element - Position int -} - -func (l *Layout) Setup(g *gocui.Gui) error { - - maxX, maxY := g.Size() - - if v, err := g.SetView("title", -1, -1, maxX, 1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - v.Frame = false - //fmt.Fprintf(v, "%v %v",l.Title, l.Position) - - } - - if v, err := g.SetView("elements", -1, 0, maxX, maxY-2); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Clear() - v.Frame = false - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - if _, err := g.SetCurrentView("elements"); err != nil { - return err - } - } - l.Render(g) - return nil -} - -func (l *Layout) Render(g *gocui.Gui) error { - maxX, maxY := g.Size() - - colorTitle := color.New(color.BgMagenta).Sprintf - // - colorTitle2 := color.New(color.FgWhite, color.BgMagenta, color.Bold).Sprintf - if v, err := g.SetCurrentView("title"); err == nil { - v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - title := "cwtch ♥ anonymity" - center := (maxX / 2) - (len(title) / 2) - - for i := 0; i < center; i++ { - v.Write([]byte(colorTitle(" "))) - } - - v.Write([]byte(colorTitle2(title))) - - //v.Write([]byte(colorTitle2())) - for i := len(title); i < maxX+1; i++ { - v.Write([]byte(colorTitle(" "))) - } - /** pandora := "cwtch " + l.Title - for i := 0; i < len([]rune(pandora)); i++ { - v.SetRune(i+1, 0, rune(pandora[i]), gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) - - } - v.SetRune(7, 0, '♥', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)*/ - - } - - if v, err := g.SetCurrentView("elements"); err == nil { - v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - numElementsToShow := (maxY - 3) - - listSelected := color.New(color.BgWhite, color.FgWhite, color.Bold).Sprintf - listAlt1 := color.New(color.BgWhite, color.FgBlack).Sprintf - //listAlt2:= color.New(color.BgBlack, color.FgWhite, color.Faint).Sprintf - fmt.Fprintf(v, "%v", listSelected(" "+l.Elements[l.Position].Contents)) - for i := 0; i < maxX+1; i++ { - v.Write([]byte(listSelected(" "))) - } - fmt.Fprintln(v, "") - - for i := l.Position + 1; i < numElementsToShow; i++ { - if i < len(l.Elements) { - // if i%2 != 0 { - fmt.Fprintf(v, "%v", listAlt1(" "+l.Elements[i].Contents)) - for i := 0; i < maxX; i++ { - v.Write([]byte(listAlt1(" "))) - } - fmt.Fprintln(v, "") - - } - - } - } - - return nil - -} - -func (l *Layout) MoveUp() { - if l.Position > 0 { - l.Position -= 1 - } -} - -func (l *Layout) MoveDown() { - if l.Position < len(l.Elements)-1 { - l.Position += 1 - } -} diff --git a/ui/screen.go b/ui/screen.go deleted file mode 100644 index cf9f447..0000000 --- a/ui/screen.go +++ /dev/null @@ -1,9 +0,0 @@ -package ui - -type Screen interface { - Title() string - MoveUp() - MoveDown() - Transition() string - Action() Action -} diff --git a/ui/state.go b/ui/state.go deleted file mode 100644 index 9e87b9c..0000000 --- a/ui/state.go +++ /dev/null @@ -1,149 +0,0 @@ -package ui - -/** -import ( - "fmt" - "github.com/fatih/color" - "github.com/jroimartin/gocui" -) - -type Screen struct { - Title string - Log []string - LogPosition int - IsGroup bool - IsWelcome bool -} - -func (sc *Screen) AppendToLog(log string) { - sc.Log = append(sc.Log, log) - sc.LogPosition++ -} - -type State struct { - CurrentScreen int - Screens []Screen - Gui *gocui.Gui -} - -func (s *State) Render() error { - g := s.Gui - maxX, maxY := g.Size() - - if v, err := g.SetCurrentView("out"); err == nil { - v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - screen := s.Screens[s.CurrentScreen] - for i := 0; i < len(screen.Log); i++ { - v.Write([]byte(screen.Log[i] + "\n")) - } - } - - if v, err := g.SetCurrentView("menu"); err == nil { - s.RenderMenu(g, v, s.Screens[s.CurrentScreen]) - } else { - return err - } - - if _, err := g.SetCurrentView("cmd"); err == nil { - for i := 0; i < maxX; i++ { - g.SetRune(i, maxY-2, '─', gocui.ColorWhite, gocui.ColorBlack) - } - for i := 0; i < maxX; i++ { - g.SetRune(i, 0, ' ', gocui.ColorWhite, gocui.ColorMagenta) - } - g.SetRune(0, maxY-1, '>', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) - g.SetRune(1, maxY-1, ' ', gocui.ColorBlack, 0) - } else { - return err - } - - pandora := "cwtch " + s.Screens[s.CurrentScreen].Title - for i := 0; i < len([]rune(pandora)); i++ { - g.SetRune(i+1, 0, rune(pandora[i]), gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) - - } - g.SetRune(7, 0, '♥', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) - return nil -} - -func (s *State) NewScreen(title string, isgroup bool) { - s.Screens = append(s.Screens, Screen{Title: title, IsGroup: isgroup, IsWelcome: (len(s.Screens) == 0)}) -} - -func (s *State) GetScreen(num int) *Screen { - if len(s.Screens) > num { - return &s.Screens[num] - } - return &s.Screens[0] -} - -func (s *State) PreviousScreen() { - if len(s.Screens) > 1 { - if s.CurrentScreen == 0 { - s.CurrentScreen = len(s.Screens) - 1 - } else { - s.CurrentScreen -= 1 - } - } -} - -func (s *State) GotoScreen(num int) { - if len(s.Screens) > num { - s.CurrentScreen = num - s.Render() - } - -} - -func (s *State) NextScreen() { - if len(s.Screens) == 1 || s.CurrentScreen == len(s.Screens)-1 { - s.CurrentScreen = 0 - } else { - s.CurrentScreen += 1 - } -} - -func String(c int, str string) string { - return fmt.Sprintf("\x1b[38;5;%dm%s\x1b[0m", c, str) -} - -func (s *State) RenderMenu(g *gocui.Gui, v *gocui.View, sc Screen) { - - v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - - colorGroupChat := color.New(color.FgMagenta, color.Bold).Sprintf - colorPersonalChat := color.New(color.FgGreen, color.Bold).Sprintf - colorMenu := color.New(color.FgYellow, color.Bold).Sprintf - colorNumber := color.New(color.FgWhite).Sprintf - colorSelected := color.New(color.FgGreen, color.Bold).Sprintf - - var group string - if sc.IsGroup { - group = colorGroupChat("G") - } else if !sc.IsWelcome { - group = colorPersonalChat("P") - } else { - group = colorMenu("W") - } - - var screenSelection string - - for i := 0; i < s.CurrentScreen; i++ { - screenSelection += colorNumber("%d ", i) - } - screenSelection += colorSelected("*%d:%s ", s.CurrentScreen, sc.Title) - - for i := s.CurrentScreen + 1; i < len(s.Screens); i++ { - screenSelection += colorNumber("%d ", i) - } - - v.Write([]byte(fmt.Sprintf("⣿ %s [%s] ⡇%s", - colorMenu("MENU"), - group, - screenSelection))) - -}*/ diff --git a/ux/main.go b/ux/main.go deleted file mode 100644 index 412148c..0000000 --- a/ux/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - //"fmt" - "git.mascherari.press/cwtch/ui" - "github.com/jroimartin/gocui" - "log" -) - -func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit -} - -var layout ui.Layout - -func cursorDown(g *gocui.Gui, v *gocui.View) error { - layout.MoveDown() - // layout.Position = 2 - layout.Render(g) - return nil -} - -func cursorUp(g *gocui.Gui, v *gocui.View) error { - layout.MoveUp() - // layout.Position = 3 - return layout.Render(g) -} - -func main() { - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - log.Panicln(err) - } - defer g.Close() - - g.Cursor = false - layout.Title = "Cwtch" - layout.Position = 1 - layout.Elements = []ui.Element{{"Alice"}, {"Bob"}, {"Carol"}, {"Malory"}} - - g.SetManagerFunc(layout.Setup) - - if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { - //return err - } - if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { - //return err - } - - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - log.Panicln(err) - } - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) - } -}