Browse Source

Massive clean up

pull/8/head
Sarah Jamie Lewis 2 years ago
parent
commit
7e8ad39637
25 changed files with 55 additions and 1390 deletions
  1. +3
    -0
      .gitignore
  2. +0
    -106
      editor.go
  3. +0
    -231
      main.go
  4. +4
    -3
      model/group.go
  5. +12
    -24
      model/profile.go
  6. +1
    -1
      model/profile_test
  7. +20
    -7
      model/profile_test.go
  8. +0
    -314
      papers/metadata-resistant-group-chat.md
  9. +0
    -1
      papers/metadata-resistant-protocols.md
  10. +0
    -83
      peer/client.go
  11. +1
    -1
      peer/cwtch_peer.go
  12. +9
    -0
      peer/send/peer_send_channel_test.go
  13. +1
    -1
      peer/test_profile
  14. +0
    -141
      protocol.md
  15. +1
    -0
      server/ms.test
  16. +3
    -2
      testing/cwtch_peer_server_intergration_test.go
  17. +0
    -18
      todo.md
  18. +0
    -15
      ui/action.go
  19. +0
    -33
      ui/chat_screen.go
  20. +0
    -37
      ui/contact_screen.go
  21. +0
    -30
      ui/contact_screen_test.go
  22. +0
    -127
      ui/layout.go
  23. +0
    -9
      ui/screen.go
  24. +0
    -149
      ui/state.go
  25. +0
    -57
      ux/main.go

+ 3
- 0
.gitignore View File

@@ -1,2 +1,5 @@
*.out
.idea
*private_key*
*.messages
*.test

+ 0
- 106
editor.go View File

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

+ 0
- 231
main.go View File

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

+ 4
- 3
model/group.go View File

@@ -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 ...


+ 12
- 24
model/profile.go View File

@@ -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) {
@@ -91,14 +80,13 @@ func (p *Profile) AddContact(onion string, profile PublicProfile) {
}

// 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),


+ 1
- 1
model/profile_test View File

@@ -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":{}}
{"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":{}}

+ 20
- 7
model/profile_test.go View File

@@ -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!")
}
}

+ 0
- 314
papers/metadata-resistant-group-chat.md View File

@@ -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.







































































+ 0
- 1
papers/metadata-resistant-protocols.md View File

@@ -1 +0,0 @@
# Metdata-Resistant Protocols: An Overview

+ 0
- 83
peer/client.go View File

@@ -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) {

}

+ 1
- 1
peer/cwtch_peer.go View File

@@ -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,
}


+ 9
- 0
peer/send/peer_send_channel_test.go View File

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

}

+ 1
- 1
peer/test_profile View File

@@ -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":{}}}
{"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":{}}}

+ 0
- 141
protocol.md View File

@@ -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.


+ 1
- 0
server/ms.test View File

@@ -1 +1,2 @@
{"ciphertext":"SGVsbG8gdGhpcyBpcyBhIGZhaXJseSBhdmVyYWdlIGxlbmd0aCBtZXNzYWdlIHRoYXQgd2UgYXJlIHdyaXRpbmcgaGVyZS4="}
{"ciphertext":"SGVsbG8gdGhpcyBpcyBhIGZhaXJseSBhdmVyYWdlIGxlbmd0aCBtZXNzYWdlIHRoYXQgd2UgYXJlIHdyaXRpbmcgaGVyZS4="}

peer/cwtch_peer_server_intergration_test.go → testing/cwtch_peer_server_intergration_test.go View File

@@ -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")

+ 0
- 18
todo.md View File

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

+ 0
- 15
ui/action.go View File

@@ -1,15 +0,0 @@
package ui

type ActionType int

const (
NONE ActionType = iota
OPEN
SEND
)

type Action struct {
Type ActionType
ID string
Context string
}

+ 0
- 33
ui/chat_screen.go View File

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

+ 0
- 37
ui/contact_screen.go View File

@@ -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, "", ""}
}

+ 0
- 30
ui/contact_screen_test.go View File

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

}

+ 0
- 127
ui/layout.go View File

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

+ 0
- 9
ui/screen.go View File

@@ -1,9 +0,0 @@
package ui

type Screen interface {
Title() string
MoveUp()
MoveDown()
Transition() string
Action() Action
}

+ 0
- 149
ui/state.go View File

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

}*/

+ 0
- 57
ux/main.go View File

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

Loading…
Cancel
Save