forked from cwtch.im/cwtch
deniable profiles implementation with tests; app extended to handle multiple peers
This commit is contained in:
parent
f217533044
commit
a1d37c3d14
|
@ -3,5 +3,3 @@
|
||||||
*private_key*
|
*private_key*
|
||||||
*.messages
|
*.messages
|
||||||
*.test
|
*.test
|
||||||
*/*test_*
|
|
||||||
*/*_test*
|
|
||||||
|
|
130
app/app.go
130
app/app.go
|
@ -2,49 +2,58 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cwtch.im/cwtch/connectivity/tor"
|
"cwtch.im/cwtch/connectivity/tor"
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/peer"
|
"cwtch.im/cwtch/peer"
|
||||||
"errors"
|
"cwtch.im/cwtch/storage"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ProfileFileName is the file used by app to store profiles. Clients of app can use this to check if it has
|
||||||
|
// been initialized or not. Once app has been run once, it will exist (from ProfileStore)
|
||||||
|
ProfileFileName = "profiles"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Application is a facade over a cwtchPeer that provides some wrapping logic.
|
// Application is a facade over a cwtchPeer that provides some wrapping logic.
|
||||||
type Application struct {
|
type application struct {
|
||||||
Peer peer.CwtchPeerInterface
|
peers map[string]peer.CwtchPeer
|
||||||
TorManager *tor.Manager
|
profileStore storage.ProfileStore
|
||||||
|
torManager *tor.Manager
|
||||||
directory string
|
directory string
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
|
||||||
|
type Application interface {
|
||||||
|
InitProfiles(password string) error
|
||||||
|
InitDeniableProfiles(password string) error
|
||||||
|
CreatePeer(name string) peer.CwtchPeer
|
||||||
|
|
||||||
|
GetPeer(onion string) peer.CwtchPeer
|
||||||
|
ListPeers() map[string]string
|
||||||
|
|
||||||
|
Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
||||||
func NewApp(appDirectory string, torPath string) (*Application, error) {
|
func NewApp(appDirectory string, torPath string) (Application, error) {
|
||||||
log.Printf("NewApp(%v, %v)\n", appDirectory, torPath)
|
log.Printf("NewApp(%v, %v)\n", appDirectory, torPath)
|
||||||
app := &Application{Peer: nil, directory: appDirectory}
|
app := &application{peers: make(map[string]peer.CwtchPeer), directory: appDirectory}
|
||||||
os.MkdirAll(path.Join(appDirectory, "tor"), 0700)
|
os.MkdirAll(path.Join(appDirectory, "tor"), 0700)
|
||||||
err := app.startTor(torPath)
|
err := app.startTor(torPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
app.profileStore = storage.NewProfileStore(path.Join(appDirectory, ProfileFileName), storage.DataBlockSizeMedium, storage.NumProfileBlocks, 2)
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProfile creates a new cwtchPeer with a given name.
|
|
||||||
func (app *Application) NewProfile(name string, password string) error {
|
|
||||||
log.Printf("NewProfile(%v, %v)\n", name, password)
|
|
||||||
if app.Peer != nil {
|
|
||||||
return errors.New("Profile already created")
|
|
||||||
}
|
|
||||||
app.Peer = peer.NewCwtchPeer(name, password, path.Join(app.directory, name+".json"))
|
|
||||||
err := app.Peer.Save()
|
|
||||||
if err == nil {
|
|
||||||
err = app.startPeer()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// startTor will create a local torrc if needed
|
// startTor will create a local torrc if needed
|
||||||
func (app *Application) startTor(torPath string) error {
|
func (app *application) startTor(torPath string) error {
|
||||||
// Creating a local cwtch tor server config for the user
|
// Creating a local cwtch tor server config for the user
|
||||||
// creating $app.directory/torrc file
|
// creating $app.directory/torrc file
|
||||||
// SOCKSPort socksPort
|
// SOCKSPort socksPort
|
||||||
|
@ -64,35 +73,86 @@ func (app *Application) startTor(torPath string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
app.TorManager = tm
|
app.torManager = tm
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetProfile loads an existing profile from the given filename.
|
// InitProfiles will attempt to load the normal portion of profile storage with the supplied password
|
||||||
func (app *Application) SetProfile(filename string, password string) error {
|
func (app *application) InitProfiles(password string) error {
|
||||||
if app.Peer == nil {
|
profiles, err := app.profileStore.InitializeProfileGroup(storage.GroupMaster, password)
|
||||||
profile, err := peer.LoadCwtchPeer(path.Join(app.directory, filename), password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
app.Peer = profile
|
|
||||||
return app.startPeer()
|
app.loadProfiles(profiles)
|
||||||
}
|
return nil
|
||||||
return errors.New("profile is already loaded, to load a different profile you will need to restart the application")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) startPeer() error {
|
// InitDeniableProfiles will attempt to load the deniable portion of profile storage with the supplied password
|
||||||
|
func (app *application) InitDeniableProfiles(password string) error {
|
||||||
|
profiles, err := app.profileStore.InitializeProfileGroup(storage.GroupDeniable1, password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
app.loadProfiles(profiles)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) loadProfiles(profiles []*model.Profile) {
|
||||||
|
app.mutex.Lock()
|
||||||
|
for _, profile := range profiles {
|
||||||
|
fmt.Printf("init from profile: %v\n", profile.Name)
|
||||||
|
peer := peer.InitFromProfile(profile)
|
||||||
|
app.peers[peer.GetProfile().Onion] = peer
|
||||||
|
app.startPeer(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProfile creates a new cwtchPeer with a given name.
|
||||||
|
func (app *application) CreatePeer(name string) peer.CwtchPeer {
|
||||||
|
app.mutex.Lock()
|
||||||
|
p := peer.NewCwtchPeer(name)
|
||||||
|
|
||||||
|
app.profileStore.AddProfile(storage.GroupMaster, p.GetProfile())
|
||||||
|
app.peers[p.GetProfile().Onion] = p
|
||||||
|
|
||||||
|
app.startPeer(p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) startPeer(peer peer.CwtchPeer) {
|
||||||
go func() {
|
go func() {
|
||||||
e := app.Peer.Listen()
|
e := peer.Listen()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
log.Panic(e)
|
log.Panic(e)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPeers returns a map of onions to their profile's Name
|
||||||
|
func (app *application) ListPeers() map[string]string {
|
||||||
|
keys := map[string]string{}
|
||||||
|
for k, p := range app.peers {
|
||||||
|
keys[k] = p.GetProfile().Name
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeer returns a Peer for a given onion address
|
||||||
|
func (app *application) GetPeer(onion string) peer.CwtchPeer {
|
||||||
|
if peer, ok := app.peers[onion]; ok {
|
||||||
|
return peer
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerRequest attempts to setup peer relationship with the given onion address.`
|
// Shutdown shutsdown all peers of an app and then the tormanager
|
||||||
func (app *Application) PeerRequest(onion string) {
|
func (app *application) Shutdown() {
|
||||||
app.Peer.PeerWithOnion(onion)
|
for _, peer := range app.peers {
|
||||||
|
peer.Shutdown()
|
||||||
|
}
|
||||||
|
app.torManager.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
257
app/cli/main.go
257
app/cli/main.go
|
@ -9,6 +9,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
|
peer2 "cwtch.im/cwtch/peer"
|
||||||
|
"cwtch.im/cwtch/peer/connections"
|
||||||
|
"errors"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -18,32 +21,37 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var app *app2.Application
|
var app app2.Application
|
||||||
|
var peer peer2.CwtchPeer
|
||||||
|
|
||||||
var suggestions = []prompt.Suggest{
|
var suggestions = []prompt.Suggest{
|
||||||
{Text: "new-profile", Description: "create a new profile in ~/.cwtch/$USERNAME.json"},
|
{Text: "new-profile", Description: "create a new profile"},
|
||||||
{Text: "load-profile", Description: "load a new profile"},
|
{Text: "select-profile", Description: "selects an active profile to use"},
|
||||||
|
{Text: "list-profiles", Description: "list active profiles"},
|
||||||
|
{Text: "load-profiles", Description: "loads profiles"},
|
||||||
{Text: "quit", Description: "quit cwtch"},
|
{Text: "quit", Description: "quit cwtch"},
|
||||||
{Text: "info", Description: "show user info"},
|
{Text: "info", Description: "show user info"},
|
||||||
{Text: "servers", Description: "retrieve a list of servers and their connection status"},
|
{Text: "servers", Description: "retrieve a list of servers and their connection status"},
|
||||||
{Text: "peers", Description: "retrieve a list of peers and their connection status"},
|
{Text: "peers", Description: "retrieve a list of peers and their connection status"},
|
||||||
{Text: "contacts", Description: "retrieve a list of contacts"},
|
|
||||||
{Text: "groups", Description: "retrieve a list of groups"},
|
|
||||||
{Text: "export-group", Description: "export a group invite: prints as a string"},
|
|
||||||
{Text: "send", Description: "send a message to a group"},
|
|
||||||
{Text: "timeline", Description: "read the timeline of a given group"},
|
|
||||||
{Text: "accept-invite", Description: "accept the invite of a group"},
|
|
||||||
{Text: "invite", Description: "invite a new contact"},
|
{Text: "invite", Description: "invite a new contact"},
|
||||||
{Text: "invite-to-group", Description: "invite an existing contact to join an existing group"},
|
|
||||||
{Text: "new-group", Description: "create a new group"},
|
|
||||||
{Text: "help", Description: "print list of commands"},
|
|
||||||
{Text: "trust", Description: "trust a peer"},
|
{Text: "trust", Description: "trust a peer"},
|
||||||
{Text: "block", Description: "block a peer - you will no longer see messages or connect to this peer"},
|
{Text: "block", Description: "block a peer - you will no longer see messages or connect to this peer"},
|
||||||
|
{Text: "contacts", Description: "retrieve a list of contacts"},
|
||||||
|
{Text: "groups", Description: "retrieve a list of groups"},
|
||||||
|
{Text: "new-group", Description: "create a new group"},
|
||||||
|
{Text: "invite-to-group", Description: "invite an existing contact to join an existing group"},
|
||||||
|
{Text: "accept-invite", Description: "accept the invite of a group"},
|
||||||
|
{Text: "export-group", Description: "export a group invite: prints as a string"},
|
||||||
|
{Text: "timeline", Description: "read the timeline of a given group"},
|
||||||
|
{Text: "send", Description: "send a message to a group"},
|
||||||
|
{Text: "help", Description: "print list of commands"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var usages = map[string]string{
|
var usages = map[string]string{
|
||||||
"new-profile": "new-profile [name]",
|
"new-profile": "new-profile [name]",
|
||||||
"load-profile": "load-profile [filename]",
|
"load-profiles": "",
|
||||||
|
"list-profiles": "",
|
||||||
|
"select-profile": "select-profile [onion]",
|
||||||
"quit": "",
|
"quit": "",
|
||||||
"servers": "",
|
"servers": "",
|
||||||
"peers": "",
|
"peers": "",
|
||||||
|
@ -70,16 +78,16 @@ func completer(d prompt.Document) []prompt.Suggest {
|
||||||
return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true)
|
return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.Peer == nil {
|
if peer == nil {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
w := d.CurrentLine()
|
w := d.CurrentLine()
|
||||||
if strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") || strings.HasPrefix(w, "export-group") {
|
if strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") || strings.HasPrefix(w, "export-group") {
|
||||||
s = []prompt.Suggest{}
|
s = []prompt.Suggest{}
|
||||||
groups := app.Peer.GetGroups()
|
groups := peer.GetGroups()
|
||||||
for _, groupID := range groups {
|
for _, groupID := range groups {
|
||||||
group := app.Peer.GetGroup(groupID)
|
group := peer.GetGroup(groupID)
|
||||||
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
|
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
|
||||||
}
|
}
|
||||||
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
||||||
|
@ -87,9 +95,9 @@ func completer(d prompt.Document) []prompt.Suggest {
|
||||||
|
|
||||||
if strings.HasPrefix(w, "block") || strings.HasPrefix(w, "trust") {
|
if strings.HasPrefix(w, "block") || strings.HasPrefix(w, "trust") {
|
||||||
s = []prompt.Suggest{}
|
s = []prompt.Suggest{}
|
||||||
contacts := app.Peer.GetContacts()
|
contacts := peer.GetContacts()
|
||||||
for _, onion := range contacts {
|
for _, onion := range contacts {
|
||||||
contact := app.Peer.GetContact(onion)
|
contact := peer.GetContact(onion)
|
||||||
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
|
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
|
||||||
}
|
}
|
||||||
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
||||||
|
@ -99,17 +107,17 @@ func completer(d prompt.Document) []prompt.Suggest {
|
||||||
|
|
||||||
if d.FindStartOfPreviousWordWithSpace() == 0 {
|
if d.FindStartOfPreviousWordWithSpace() == 0 {
|
||||||
s = []prompt.Suggest{}
|
s = []prompt.Suggest{}
|
||||||
contacts := app.Peer.GetContacts()
|
contacts := peer.GetContacts()
|
||||||
for _, onion := range contacts {
|
for _, onion := range contacts {
|
||||||
contact := app.Peer.GetContact(onion)
|
contact := peer.GetContact(onion)
|
||||||
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
|
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
|
||||||
}
|
}
|
||||||
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
||||||
}
|
}
|
||||||
s = []prompt.Suggest{}
|
s = []prompt.Suggest{}
|
||||||
groups := app.Peer.GetGroups()
|
groups := peer.GetGroups()
|
||||||
for _, groupID := range groups {
|
for _, groupID := range groups {
|
||||||
group := app.Peer.GetGroup(groupID)
|
group := peer.GetGroup(groupID)
|
||||||
if group.Owner == "self" {
|
if group.Owner == "self" {
|
||||||
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
|
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
|
||||||
}
|
}
|
||||||
|
@ -119,9 +127,9 @@ func completer(d prompt.Document) []prompt.Suggest {
|
||||||
|
|
||||||
if strings.HasPrefix(w, "accept-invite") {
|
if strings.HasPrefix(w, "accept-invite") {
|
||||||
s = []prompt.Suggest{}
|
s = []prompt.Suggest{}
|
||||||
groups := app.Peer.GetGroups()
|
groups := peer.GetGroups()
|
||||||
for _, groupID := range groups {
|
for _, groupID := range groups {
|
||||||
group := app.Peer.GetGroup(groupID)
|
group := peer.GetGroup(groupID)
|
||||||
if group.Accepted == false {
|
if group.Accepted == false {
|
||||||
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
|
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
|
||||||
}
|
}
|
||||||
|
@ -129,9 +137,46 @@ func completer(d prompt.Document) []prompt.Suggest {
|
||||||
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(w, "select-profile") {
|
||||||
|
s = []prompt.Suggest{}
|
||||||
|
peerlist := app.ListPeers()
|
||||||
|
for onion, peername := range peerlist {
|
||||||
|
s = append(s, prompt.Suggest{Text: onion, Description: peername})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptFirstPassword() (string, error) {
|
||||||
|
fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n")
|
||||||
|
|
||||||
|
password := ""
|
||||||
|
failcount := 0
|
||||||
|
for ; failcount < 3; failcount++ {
|
||||||
|
fmt.Print("Enter a password to encrypt your profiles: ")
|
||||||
|
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
|
||||||
|
if string(bytePassword) == "" {
|
||||||
|
fmt.Print("\nBlank password not allowed.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Print("\nRe-enter password: ")
|
||||||
|
bytePassword2, _ := terminal.ReadPassword(int(syscall.Stdin))
|
||||||
|
if bytes.Equal(bytePassword, bytePassword2) {
|
||||||
|
password = string(bytePassword)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
fmt.Print("\nPASSWORDS DIDN'T MATCH! Try again.\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failcount >= 3 {
|
||||||
|
fmt.Printf("Error: Your password entries must match!\n")
|
||||||
|
return "", errors.New("Password not accepted")
|
||||||
|
}
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
cwtch :=
|
cwtch :=
|
||||||
|
@ -184,15 +229,30 @@ func main() {
|
||||||
log.Fatalf("\nError: could not load current user: %v\n", err)
|
log.Fatalf("\nError: could not load current user: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path.Join(usr.HomeDir, ".cwtch", app2.ProfileFileName)); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("This appears to be your first time using Cwtch.\nPlease set a master password for your profiles\n")
|
||||||
|
password, err := promptFirstPassword()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: Failed to setup password!]n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
app, err = app2.NewApp(path.Join(usr.HomeDir, ".cwtch"), torPath)
|
app, err = app2.NewApp(path.Join(usr.HomeDir, ".cwtch"), torPath)
|
||||||
|
app.InitProfiles(password)
|
||||||
|
fmt.Printf("You should `new-profile` to create your first cwtch peer\n")
|
||||||
|
} else {
|
||||||
|
app, err = app2.NewApp(path.Join(usr.HomeDir, ".cwtch"), torPath)
|
||||||
|
fmt.Printf("You should run `load-profiles` to load profiles from storage\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
peer = nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error initializing application: %v", err)
|
log.Fatalf("Error initializing application: %v", err)
|
||||||
}
|
}
|
||||||
var history []string
|
var history []string
|
||||||
for !quit {
|
for !quit {
|
||||||
profile := "unset"
|
profile := "unset"
|
||||||
if app.Peer != nil {
|
if peer != nil {
|
||||||
profile = app.Peer.GetProfile().Name
|
profile = peer.GetProfile().Name
|
||||||
}
|
}
|
||||||
prmpt := fmt.Sprintf("cwtch [%v]> ", profile)
|
prmpt := fmt.Sprintf("cwtch [%v]> ", profile)
|
||||||
|
|
||||||
|
@ -203,128 +263,112 @@ func main() {
|
||||||
commands := strings.Split(text[0:], " ")
|
commands := strings.Split(text[0:], " ")
|
||||||
history = append(history, text)
|
history = append(history, text)
|
||||||
|
|
||||||
if app.Peer == nil {
|
if peer == nil {
|
||||||
if commands[0] != "help" && commands[0] != "quit" && commands[0] != "new-profile" && commands[0] != "load-profile" {
|
if commands[0] != "help" && commands[0] != "quit" && commands[0] != "select-profile" && commands[0] != "load-profiles" && commands[0] != "new-profile" && commands[0] != "list-profiles" {
|
||||||
fmt.Printf("Profile needs to be set\n")
|
fmt.Printf("Profile needs to be set with `select-profile` or `new-profile`\n")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch commands[0] {
|
switch commands[0] {
|
||||||
case "quit":
|
case "quit":
|
||||||
if app.Peer != nil {
|
if peer != nil {
|
||||||
app.Peer.Save()
|
peer.Save()
|
||||||
}
|
}
|
||||||
quit = true
|
quit = true
|
||||||
case "new-profile":
|
case "new-profile":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n")
|
p := app.CreatePeer(commands[1])
|
||||||
|
peer = p
|
||||||
password := ""
|
|
||||||
failcount := 0
|
|
||||||
for ; failcount < 3; failcount++ {
|
|
||||||
fmt.Print("Enter a password to encrypt the profile: ")
|
|
||||||
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
|
|
||||||
if string(bytePassword) == "" {
|
|
||||||
fmt.Print("\nBlank password not allowed.")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Print("\nRe-enter password: ")
|
|
||||||
bytePassword2, _ := terminal.ReadPassword(int(syscall.Stdin))
|
|
||||||
if bytes.Equal(bytePassword, bytePassword2) {
|
|
||||||
password = string(bytePassword)
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
fmt.Print("\nPASSWORDS DIDN'T MATCH! Try again.\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if failcount >= 3 {
|
|
||||||
fmt.Printf("Error creating profile for %v: Your password entries must match!\n", commands[1])
|
|
||||||
} else {
|
|
||||||
err := app.NewProfile(commands[1], password)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Printf("\nNew profile created for %v\n", commands[1])
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\nError creating profile for %v: %v\n", commands[1], err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error creating NewProfile, usage: %s\n", usages["new-profile"])
|
fmt.Printf("Error creating NewProfile, usage: %s\n", usages["new-profile"])
|
||||||
}
|
}
|
||||||
case "load-profile":
|
case "load-profiles":
|
||||||
if len(commands) == 2 {
|
fmt.Print("Enter a password to decrypt the profiles: ")
|
||||||
fmt.Print("Enter a password to decrypt the profile: ")
|
|
||||||
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
|
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||||
err = app.SetProfile(commands[1]+".json", string(bytePassword))
|
err = app.InitProfiles(string(bytePassword))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf("\nLoaded profile for %v\n", commands[1])
|
profiles := app.ListPeers()
|
||||||
|
fmt.Printf("\n%v profiles active now\n", len(profiles))
|
||||||
|
fmt.Printf("You should run `select-profile` to use a profile\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\nError loading profile for %v: %v\n", commands[1], err)
|
fmt.Printf("\nError loading profiles: %v\n", err)
|
||||||
|
}
|
||||||
|
case "list-profiles":
|
||||||
|
peerlist := app.ListPeers()
|
||||||
|
for onion, peername := range peerlist {
|
||||||
|
fmt.Printf(" %v\t%v\n", onion, peername)
|
||||||
|
}
|
||||||
|
case "select-profile":
|
||||||
|
if len(commands) == 2 {
|
||||||
|
p := app.GetPeer(commands[1])
|
||||||
|
if p == nil {
|
||||||
|
fmt.Printf("Error: profile '%v' does not exist\n", commands[1])
|
||||||
|
} else {
|
||||||
|
peer = p
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error Loading profile, usage: %s\n", usages["load-profile"])
|
fmt.Printf("Error selecting profile, usage: %s\n", usages["select-profile"])
|
||||||
}
|
}
|
||||||
|
|
||||||
case "info":
|
case "info":
|
||||||
if app.Peer != nil {
|
if peer != nil {
|
||||||
fmt.Printf("Address cwtch:%v\n", app.Peer.GetProfile().Onion)
|
fmt.Printf("Address cwtch:%v\n", peer.GetProfile().Onion)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Profile needs to be set\n")
|
fmt.Printf("Profile needs to be set\n")
|
||||||
}
|
}
|
||||||
case "invite":
|
case "invite":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
fmt.Printf("Inviting cwtch:%v\n", commands[1])
|
fmt.Printf("Inviting cwtch:%v\n", commands[1])
|
||||||
app.PeerRequest(commands[1])
|
peer.PeerWithOnion(commands[1])
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error inviting peer, usage: %s\n", usages["invite"])
|
fmt.Printf("Error inviting peer, usage: %s\n", usages["invite"])
|
||||||
}
|
}
|
||||||
case "peers":
|
case "peers":
|
||||||
peers := app.Peer.GetPeers()
|
peers := peer.GetPeers()
|
||||||
for p, s := range peers {
|
for p, s := range peers {
|
||||||
fmt.Printf("Name: %v Status: %v\n", p, s)
|
fmt.Printf("Name: %v Status: %v\n", p, connections.ConnectionStateName[s])
|
||||||
}
|
}
|
||||||
case "servers":
|
case "servers":
|
||||||
servers := app.Peer.GetServers()
|
servers := peer.GetServers()
|
||||||
for s, st := range servers {
|
for s, st := range servers {
|
||||||
fmt.Printf("Name: %v Status: %v\n", s, st)
|
fmt.Printf("Name: %v Status: %v\n", s, st)
|
||||||
}
|
}
|
||||||
case "contacts":
|
case "contacts":
|
||||||
contacts := app.Peer.GetContacts()
|
contacts := peer.GetContacts()
|
||||||
for _, onion := range contacts {
|
for _, onion := range contacts {
|
||||||
c := app.Peer.GetContact(onion)
|
c := peer.GetContact(onion)
|
||||||
fmt.Printf("Name: %v Onion: %v Trusted: %v\n", c.Name, c.Onion, c.Trusted)
|
fmt.Printf("Name: %v Onion: %v Trusted: %v\n", c.Name, c.Onion, c.Trusted)
|
||||||
}
|
}
|
||||||
case "groups":
|
case "groups":
|
||||||
for _, gid := range app.Peer.GetGroups() {
|
for _, gid := range peer.GetGroups() {
|
||||||
g := app.Peer.GetGroup(gid)
|
g := peer.GetGroup(gid)
|
||||||
fmt.Printf("Group Id: %v Owner: %v Accepted:%v\n", gid, g.Owner, g.Accepted)
|
fmt.Printf("Group Id: %v Owner: %v Accepted:%v\n", gid, g.Owner, g.Accepted)
|
||||||
}
|
}
|
||||||
case "trust":
|
case "trust":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
app.Peer.TrustPeer(commands[1])
|
peer.TrustPeer(commands[1])
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error trusting peer, usage: %s\n", usages["trust"])
|
fmt.Printf("Error trusting peer, usage: %s\n", usages["trust"])
|
||||||
}
|
}
|
||||||
case "block":
|
case "block":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
app.Peer.BlockPeer(commands[1])
|
peer.BlockPeer(commands[1])
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error blocking peer, usage: %s\n", usages["trust"])
|
fmt.Printf("Error blocking peer, usage: %s\n", usages["trust"])
|
||||||
}
|
}
|
||||||
case "accept-invite":
|
case "accept-invite":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
groupID := commands[1]
|
groupID := commands[1]
|
||||||
err := app.Peer.AcceptInvite(groupID)
|
err := peer.AcceptInvite(groupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
app.Peer.Save()
|
peer.Save()
|
||||||
group := app.Peer.GetGroup(groupID)
|
group := peer.GetGroup(groupID)
|
||||||
if group == nil {
|
if group == nil {
|
||||||
fmt.Printf("Error: group does not exist\n")
|
fmt.Printf("Error: group does not exist\n")
|
||||||
} else {
|
} else {
|
||||||
app.Peer.JoinServer(group.GroupServer)
|
peer.JoinServer(group.GroupServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -333,7 +377,7 @@ func main() {
|
||||||
case "invite-to-group":
|
case "invite-to-group":
|
||||||
if len(commands) == 3 {
|
if len(commands) == 3 {
|
||||||
fmt.Printf("Inviting %v to %v\n", commands[1], commands[2])
|
fmt.Printf("Inviting %v to %v\n", commands[1], commands[2])
|
||||||
err := app.Peer.InviteOnionToGroup(commands[1], commands[2])
|
err := peer.InviteOnionToGroup(commands[1], commands[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -343,15 +387,15 @@ func main() {
|
||||||
case "new-group":
|
case "new-group":
|
||||||
if len(commands) == 2 && commands[1] != "" {
|
if len(commands) == 2 && commands[1] != "" {
|
||||||
fmt.Printf("Setting up a new group on server:%v\n", commands[1])
|
fmt.Printf("Setting up a new group on server:%v\n", commands[1])
|
||||||
id, _, err := app.Peer.StartGroup(commands[1])
|
id, _, err := peer.StartGroup(commands[1])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf("New Group [%v] created for server %v\n", id, commands[1])
|
fmt.Printf("New Group [%v] created for server %v\n", id, commands[1])
|
||||||
app.Peer.Save()
|
peer.Save()
|
||||||
group := app.Peer.GetGroup(id)
|
group := peer.GetGroup(id)
|
||||||
if group == nil {
|
if group == nil {
|
||||||
fmt.Printf("Error: group does not exist\n")
|
fmt.Printf("Error: group does not exist\n")
|
||||||
} else {
|
} else {
|
||||||
app.Peer.JoinServer(group.GroupServer)
|
peer.JoinServer(group.GroupServer)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error creating new group: %v", err)
|
fmt.Printf("Error creating new group: %v", err)
|
||||||
|
@ -362,7 +406,7 @@ func main() {
|
||||||
case "send":
|
case "send":
|
||||||
if len(commands) > 2 {
|
if len(commands) > 2 {
|
||||||
message := strings.Join(commands[2:], " ")
|
message := strings.Join(commands[2:], " ")
|
||||||
err := app.Peer.SendMessageToGroup(commands[1], message)
|
err := peer.SendMessageToGroup(commands[1], message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -371,7 +415,7 @@ func main() {
|
||||||
}
|
}
|
||||||
case "timeline":
|
case "timeline":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
group := app.Peer.GetGroup(commands[1])
|
group := peer.GetGroup(commands[1])
|
||||||
if group == nil {
|
if group == nil {
|
||||||
fmt.Printf("Error: group does not exist\n")
|
fmt.Printf("Error: group does not exist\n")
|
||||||
} else {
|
} else {
|
||||||
|
@ -382,12 +426,12 @@ func main() {
|
||||||
verified = "verified"
|
verified = "verified"
|
||||||
}
|
}
|
||||||
|
|
||||||
p := app.Peer.GetContact(m.PeerID)
|
p := peer.GetContact(m.PeerID)
|
||||||
name := "unknown"
|
name := "unknown"
|
||||||
if p != nil {
|
if p != nil {
|
||||||
name = p.Name
|
name = p.Name
|
||||||
} else if app.Peer.GetProfile().Onion == m.PeerID {
|
} else if peer.GetProfile().Onion == m.PeerID {
|
||||||
name = app.Peer.GetProfile().Name
|
name = peer.GetProfile().Name
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%v %v (%v): %v [%s]\n", m.Timestamp, name, m.PeerID, m.Message, verified)
|
fmt.Printf("%v %v (%v): %v [%s]\n", m.Timestamp, name, m.PeerID, m.Message, verified)
|
||||||
|
@ -398,31 +442,31 @@ func main() {
|
||||||
}
|
}
|
||||||
case "export-group":
|
case "export-group":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
group := app.Peer.GetGroup(commands[1])
|
group := peer.GetGroup(commands[1])
|
||||||
if group == nil {
|
if group == nil {
|
||||||
fmt.Printf("Error: group does not exist\n")
|
fmt.Printf("Error: group does not exist\n")
|
||||||
} else {
|
} else {
|
||||||
invite, _ := app.Peer.ExportGroup(commands[1])
|
invite, _ := peer.ExportGroup(commands[1])
|
||||||
fmt.Printf("Invite: %v\n", invite)
|
fmt.Printf("Invite: %v\n", invite)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Error exporting group, usage: %s\n", usages["export-group"])
|
fmt.Printf("Error exporting group, usage: %s\n", usages["export-group"])
|
||||||
}
|
}
|
||||||
case "save":
|
case "save":
|
||||||
app.Peer.Save()
|
peer.Save()
|
||||||
case "help":
|
case "help":
|
||||||
for _, command := range suggestions {
|
for _, command := range suggestions {
|
||||||
fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text])
|
fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text])
|
||||||
}
|
}
|
||||||
case "sendlots":
|
case "sendlots":
|
||||||
if len(commands) == 2 {
|
if len(commands) == 2 {
|
||||||
group := app.Peer.GetGroup(commands[1])
|
group := peer.GetGroup(commands[1])
|
||||||
if group == nil {
|
if group == nil {
|
||||||
fmt.Printf("Error: group does not exist\n")
|
fmt.Printf("Error: group does not exist\n")
|
||||||
} else {
|
} else {
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
fmt.Printf("Sending message: %v\n", i)
|
fmt.Printf("Sending message: %v\n", i)
|
||||||
err := app.Peer.SendMessageToGroup(commands[1], fmt.Sprintf("this is message %v", i))
|
err := peer.SendMessageToGroup(commands[1], fmt.Sprintf("this is message %v", i))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("could not send message %v because %v\n", i, err)
|
fmt.Printf("could not send message %v because %v\n", i, err)
|
||||||
}
|
}
|
||||||
|
@ -436,7 +480,7 @@ func main() {
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
found := false
|
found := false
|
||||||
for _, m := range timeline {
|
for _, m := range timeline {
|
||||||
if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == app.Peer.GetProfile().Onion {
|
if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == peer.GetProfile().Onion {
|
||||||
found = true
|
found = true
|
||||||
latency := m.Received.Sub(m.Timestamp)
|
latency := m.Received.Sub(m.Timestamp)
|
||||||
fmt.Printf("Latency for Message %v was %v\n", i, latency)
|
fmt.Printf("Latency for Message %v was %v\n", i, latency)
|
||||||
|
@ -459,15 +503,12 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if app.Peer != nil {
|
if peer != nil {
|
||||||
app.Peer.Save()
|
peer.Save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.TorManager != nil {
|
app.Shutdown()
|
||||||
fmt.Println("Shutting down Tor process...")
|
|
||||||
app.TorManager.Shutdown()
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"cwtch.im/cwtch/protocol"
|
"cwtch.im/cwtch/protocol"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -31,12 +36,93 @@ type Profile struct {
|
||||||
OnionPrivateKey *rsa.PrivateKey
|
OnionPrivateKey *rsa.PrivateKey
|
||||||
Groups map[string]*Group
|
Groups map[string]*Group
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
|
key [32]byte
|
||||||
|
salt [128]byte
|
||||||
|
save func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttemptLoadProfile attempts to load a profile from some encryptedBytes and a password
|
||||||
|
func AttemptLoadProfile(encryptedbytes []byte, password string, saveFn func() error) *Profile {
|
||||||
|
var dkr [32]byte
|
||||||
|
var salty [128]byte
|
||||||
|
|
||||||
|
//Separate the salt from the encrypted bytes, then generate the derived key
|
||||||
|
salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:]
|
||||||
|
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
|
||||||
|
//cast to arrays
|
||||||
|
copy(dkr[:], dk)
|
||||||
|
copy(salty[:], salt)
|
||||||
|
|
||||||
|
var profile *Profile
|
||||||
|
|
||||||
|
profile = decryptProfile(encryptedbytes, dkr)
|
||||||
|
if profile != nil {
|
||||||
|
profile.key = dkr
|
||||||
|
profile.salt = salty
|
||||||
|
profile.save = saveFn
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//decryptProfile decrypts the passed ciphertext into a Profile via the specified key if possible
|
||||||
|
func decryptProfile(ciphertext []byte, key [32]byte) *Profile {
|
||||||
|
var decryptNonce [24]byte
|
||||||
|
copy(decryptNonce[:], ciphertext[:24])
|
||||||
|
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key)
|
||||||
|
if ok {
|
||||||
|
profile := &Profile{}
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(decrypted))
|
||||||
|
err := dec.Decode(&profile)
|
||||||
|
if err == nil {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//EncryptProfile encrypts the cwtch Profile (padded to blocksize) via the specified key.
|
||||||
|
func (p *Profile) EncryptProfile(blocksize int) []byte {
|
||||||
|
var nonce [24]byte
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, _ := json.Marshal(p)
|
||||||
|
|
||||||
|
padding := make([]byte, blocksize-len(bytes))
|
||||||
|
rand.Read(padding)
|
||||||
|
bytes = append(bytes, padding...)
|
||||||
|
encrypted := secretbox.Seal(nonce[:], []byte(bytes), &nonce, &p.key)
|
||||||
|
encrypted = append(p.salt[:], encrypted...)
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save if initialized by adding the peer to a ProfileStore, will save this profile to the storage file
|
||||||
|
func (p *Profile) Save() error {
|
||||||
|
p.lock.Lock()
|
||||||
|
err := p.save()
|
||||||
|
p.lock.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnProfileStoreAdd is an event function that should be called when the Profile is added to a ProfileStore
|
||||||
|
// It takes in the necesary data to allow saving to work through the ProfileStore
|
||||||
|
func (p *Profile) OnProfileStoreAdd(key [32]byte, salt [128]byte, saveFn func() error) {
|
||||||
|
p.key = key
|
||||||
|
p.salt = salt
|
||||||
|
p.save = saveFn
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name.
|
// GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name.
|
||||||
func GenerateNewProfile(name string) *Profile {
|
func GenerateNewProfile(name string) *Profile {
|
||||||
p := new(Profile)
|
p := new(Profile)
|
||||||
p.Name = name
|
p.Name = name
|
||||||
|
p.save = func() error {
|
||||||
|
return errors.New("Profile needs to be added to a ProfileStore before Save() is initialized and can work")
|
||||||
|
}
|
||||||
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
p.Ed25519PublicKey = pub
|
p.Ed25519PublicKey = pub
|
||||||
p.Ed25519PrivateKey = priv
|
p.Ed25519PrivateKey = priv
|
||||||
|
|
|
@ -16,3 +16,8 @@ const (
|
||||||
FAILED
|
FAILED
|
||||||
KILLED
|
KILLED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ConnectionStateName allows conversaion of states to their string representations
|
||||||
|
ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Failed", "Killed"}
|
||||||
|
)
|
||||||
|
|
|
@ -1,27 +1,18 @@
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"cwtch.im/cwtch/model"
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/peer/connections"
|
"cwtch.im/cwtch/peer/connections"
|
||||||
"cwtch.im/cwtch/peer/peer"
|
"cwtch.im/cwtch/peer/peer"
|
||||||
"cwtch.im/cwtch/protocol"
|
"cwtch.im/cwtch/protocol"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/application"
|
"git.openprivacy.ca/openprivacy/libricochet-go/application"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
|
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
|
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/ulule/deepcopier"
|
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
|
||||||
"golang.org/x/crypto/pbkdf2"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -30,19 +21,16 @@ import (
|
||||||
// cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch Peer
|
// cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch Peer
|
||||||
type cwtchPeer struct {
|
type cwtchPeer struct {
|
||||||
connection.AutoConnectionHandler
|
connection.AutoConnectionHandler
|
||||||
Profile *model.Profile
|
profile *model.Profile
|
||||||
app *application.RicochetApplication
|
app *application.RicochetApplication
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
Log chan string `json:"-"`
|
Log chan string
|
||||||
connectionsManager *connections.Manager
|
connectionsManager *connections.Manager
|
||||||
profilefile string
|
|
||||||
key [32]byte
|
|
||||||
salt [128]byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CwtchPeerInterface provides us with a way of testing systems built on top of cwtch without having to
|
// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to
|
||||||
// directly implement a cwtchPeer.
|
// directly implement a cwtchPeer.
|
||||||
type CwtchPeerInterface interface {
|
type CwtchPeer interface {
|
||||||
Save() error
|
Save() error
|
||||||
PeerWithOnion(string)
|
PeerWithOnion(string)
|
||||||
InviteOnionToGroup(string, string) error
|
InviteOnionToGroup(string, string) error
|
||||||
|
@ -74,56 +62,6 @@ type CwtchPeerInterface interface {
|
||||||
Shutdown()
|
Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
// createKey derives a key and salt for use in encrypting cwtchPeers
|
|
||||||
func createKey(password string) ([32]byte, [128]byte) {
|
|
||||||
var salt [128]byte
|
|
||||||
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
|
|
||||||
|
|
||||||
var dkr [32]byte
|
|
||||||
copy(dkr[:], dk)
|
|
||||||
return dkr, salt
|
|
||||||
}
|
|
||||||
|
|
||||||
//encryptProfile encrypts the cwtchPeer via the specified key.
|
|
||||||
func encryptProfile(p *cwtchPeer, key [32]byte) []byte {
|
|
||||||
var nonce [24]byte
|
|
||||||
|
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//copy the struct, then remove the key and salt before saving the copy
|
|
||||||
cpc := &cwtchPeer{}
|
|
||||||
deepcopier.Copy(p).To(cpc)
|
|
||||||
var blankkey [32]byte
|
|
||||||
var blanksalt [128]byte
|
|
||||||
cpc.key = blankkey
|
|
||||||
cpc.salt = blanksalt
|
|
||||||
bytes, _ := json.Marshal(cpc)
|
|
||||||
encrypted := secretbox.Seal(nonce[:], []byte(bytes), &nonce, &key)
|
|
||||||
return encrypted
|
|
||||||
}
|
|
||||||
|
|
||||||
//decryptProfile decrypts the passed ciphertext into a cwtchPeer via the specified key.
|
|
||||||
func decryptProfile(ciphertext []byte, key [32]byte) (*cwtchPeer, error) {
|
|
||||||
|
|
||||||
var decryptNonce [24]byte
|
|
||||||
copy(decryptNonce[:], ciphertext[:24])
|
|
||||||
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key)
|
|
||||||
if ok {
|
|
||||||
cp := &cwtchPeer{}
|
|
||||||
err := json.Unmarshal(decrypted, &cp)
|
|
||||||
if err == nil {
|
|
||||||
return cp, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Failed to decrypt")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *cwtchPeer) setup() {
|
func (cp *cwtchPeer) setup() {
|
||||||
cp.Log = make(chan string)
|
cp.Log = make(chan string)
|
||||||
cp.connectionsManager = connections.NewConnectionsManager()
|
cp.connectionsManager = connections.NewConnectionsManager()
|
||||||
|
@ -131,13 +69,13 @@ func (cp *cwtchPeer) setup() {
|
||||||
|
|
||||||
go cp.connectionsManager.AttemptReconnections()
|
go cp.connectionsManager.AttemptReconnections()
|
||||||
|
|
||||||
for onion, profile := range cp.Profile.Contacts {
|
for onion, profile := range cp.profile.Contacts {
|
||||||
if profile.Trusted && !profile.Blocked {
|
if profile.Trusted && !profile.Blocked {
|
||||||
cp.PeerWithOnion(onion)
|
cp.PeerWithOnion(onion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range cp.Profile.Groups {
|
for _, group := range cp.profile.Groups {
|
||||||
if group.Accepted || group.Owner == "self" {
|
if group.Accepted || group.Owner == "self" {
|
||||||
cp.JoinServer(group.GroupServer)
|
cp.JoinServer(group.GroupServer)
|
||||||
}
|
}
|
||||||
|
@ -145,55 +83,24 @@ func (cp *cwtchPeer) setup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCwtchPeer creates and returns a new cwtchPeer with the given name.
|
// NewCwtchPeer creates and returns a new cwtchPeer with the given name.
|
||||||
func NewCwtchPeer(name string, password string, profilefile string) CwtchPeerInterface {
|
func NewCwtchPeer(name string) CwtchPeer {
|
||||||
cp := new(cwtchPeer)
|
cp := new(cwtchPeer)
|
||||||
cp.profilefile = profilefile
|
cp.profile = model.GenerateNewProfile(name)
|
||||||
cp.Profile = model.GenerateNewProfile(name)
|
|
||||||
cp.setup()
|
cp.setup()
|
||||||
key, salt := createKey(password)
|
|
||||||
cp.key = key
|
|
||||||
cp.salt = salt
|
|
||||||
return cp
|
return cp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the cwtchPeer profile state to a file.
|
// InitFromProfile starts a cwtchPeer from a loaded Profile
|
||||||
func (cp *cwtchPeer) Save() error {
|
func InitFromProfile(p *model.Profile) CwtchPeer {
|
||||||
cp.mutex.Lock()
|
cp := new(cwtchPeer)
|
||||||
encryptedbytes := encryptProfile(cp, cp.key)
|
cp.profile = p
|
||||||
|
cp.setup()
|
||||||
// the salt for the derived key is appended to the front of the file
|
return cp
|
||||||
encryptedbytes = append(cp.salt[:], encryptedbytes...)
|
|
||||||
err := ioutil.WriteFile(cp.profilefile, encryptedbytes, 0600)
|
|
||||||
cp.mutex.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadCwtchPeer loads an existing cwtchPeer from a file.
|
func (cp *cwtchPeer) Save() error {
|
||||||
func LoadCwtchPeer(profilefile string, password string) (CwtchPeerInterface, error) {
|
return cp.profile.Save()
|
||||||
encryptedbytes, err := ioutil.ReadFile(profilefile)
|
|
||||||
if err == nil {
|
|
||||||
var dkr [32]byte
|
|
||||||
var salty [128]byte
|
|
||||||
|
|
||||||
//Separate the salt from the encrypted bytes, then generate the derived key
|
|
||||||
salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:]
|
|
||||||
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
|
|
||||||
|
|
||||||
//cast to arrays
|
|
||||||
copy(dkr[:], dk)
|
|
||||||
copy(salty[:], salt)
|
|
||||||
|
|
||||||
var cp *cwtchPeer
|
|
||||||
cp, err = decryptProfile(encryptedbytes, dkr)
|
|
||||||
if err == nil {
|
|
||||||
cp.setup()
|
|
||||||
cp.profilefile = profilefile
|
|
||||||
cp.key = dkr
|
|
||||||
cp.salt = salty
|
|
||||||
return cp, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportGroup intializes a group from an imported source rather than a peer invite
|
// ImportGroup intializes a group from an imported source rather than a peer invite
|
||||||
|
@ -208,8 +115,8 @@ func (cp *cwtchPeer) ImportGroup(exportedInvite string) (groupID string, err err
|
||||||
if err == nil {
|
if err == nil {
|
||||||
edpk := ed25519.PublicKey(pk)
|
edpk := ed25519.PublicKey(pk)
|
||||||
onion := exportedInvite[5:21]
|
onion := exportedInvite[5:21]
|
||||||
cp.Profile.AddContact(onion, &model.PublicProfile{Name: "", Ed25519PublicKey: edpk, Trusted: true, Blocked: false, Onion: onion})
|
cp.profile.AddContact(onion, &model.PublicProfile{Name: "", Ed25519PublicKey: edpk, Trusted: true, Blocked: false, Onion: onion})
|
||||||
cp.Profile.ProcessInvite(cpp.GetGroupChatInvite(), onion)
|
cp.profile.ProcessInvite(cpp.GetGroupChatInvite(), onion)
|
||||||
return cpp.GroupChatInvite.GetGroupName(), nil
|
return cpp.GroupChatInvite.GetGroupName(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -222,11 +129,11 @@ func (cp *cwtchPeer) ImportGroup(exportedInvite string) (groupID string, err err
|
||||||
|
|
||||||
// ExportGroup serializes a group invite so it can be given offline
|
// ExportGroup serializes a group invite so it can be given offline
|
||||||
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
||||||
group := cp.Profile.GetGroupByGroupID(groupID)
|
group := cp.profile.GetGroupByGroupID(groupID)
|
||||||
if group != nil {
|
if group != nil {
|
||||||
invite, err := group.Invite()
|
invite, err := group.Invite()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
exportedInvite := "torv2" + cp.Profile.Onion + base64.StdEncoding.EncodeToString(cp.Profile.Ed25519PublicKey) + base64.StdEncoding.EncodeToString(invite)
|
exportedInvite := "torv2" + cp.profile.Onion + base64.StdEncoding.EncodeToString(cp.profile.Ed25519PublicKey) + base64.StdEncoding.EncodeToString(invite)
|
||||||
return exportedInvite, err
|
return exportedInvite, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,45 +142,45 @@ func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
||||||
|
|
||||||
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
|
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
|
||||||
func (cp *cwtchPeer) StartGroup(server string) (string, []byte, error) {
|
func (cp *cwtchPeer) StartGroup(server string) (string, []byte, error) {
|
||||||
return cp.Profile.StartGroup(server)
|
return cp.profile.StartGroup(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroups returns an unordered list of all group IDs.
|
// GetGroups returns an unordered list of all group IDs.
|
||||||
func (cp *cwtchPeer) GetGroups() []string {
|
func (cp *cwtchPeer) GetGroups() []string {
|
||||||
return cp.Profile.GetGroups()
|
return cp.profile.GetGroups()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroup returns a pointer to a specific group, nil if no group exists.
|
// GetGroup returns a pointer to a specific group, nil if no group exists.
|
||||||
func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
|
func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
|
||||||
return cp.Profile.GetGroupByGroupID(groupID)
|
return cp.profile.GetGroupByGroupID(groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContacts returns an unordered list of onions
|
// GetContacts returns an unordered list of onions
|
||||||
func (cp *cwtchPeer) GetContacts() []string {
|
func (cp *cwtchPeer) GetContacts() []string {
|
||||||
return cp.Profile.GetContacts()
|
return cp.profile.GetContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContact returns a given contact, nil is no such contact exists
|
// GetContact returns a given contact, nil is no such contact exists
|
||||||
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
|
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
|
||||||
contact, _ := cp.Profile.GetContact(onion)
|
contact, _ := cp.profile.GetContact(onion)
|
||||||
return contact
|
return contact
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfile returns the profile associated with this Peer.
|
// GetProfile returns the profile associated with this Peer.
|
||||||
// TODO While it is probably "safe", it is not really "safe", to call functions on this profile. This only exists to return things like Name and Onion,we should gate these.
|
// TODO While it is probably "safe", it is not really "safe", to call functions on this profile. This only exists to return things like Name and Onion,we should gate these.
|
||||||
func (cp *cwtchPeer) GetProfile() *model.Profile {
|
func (cp *cwtchPeer) GetProfile() *model.Profile {
|
||||||
return cp.Profile
|
return cp.profile
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerWithOnion is the entry point for cwtchPeer relationships
|
// PeerWithOnion is the entry point for cwtchPeer relationships
|
||||||
func (cp *cwtchPeer) PeerWithOnion(onion string) {
|
func (cp *cwtchPeer) PeerWithOnion(onion string) {
|
||||||
cp.connectionsManager.ManagePeerConnection(onion, cp.Profile)
|
cp.connectionsManager.ManagePeerConnection(onion, cp.profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InviteOnionToGroup kicks off the invite process
|
// InviteOnionToGroup kicks off the invite process
|
||||||
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
||||||
|
|
||||||
group := cp.Profile.GetGroupByGroupID(groupid)
|
group := cp.profile.GetGroupByGroupID(groupid)
|
||||||
if group != nil {
|
if group != nil {
|
||||||
log.Printf("Constructing invite for group: %v\n", group)
|
log.Printf("Constructing invite for group: %v\n", group)
|
||||||
invite, err := group.Invite()
|
invite, err := group.Invite()
|
||||||
|
@ -297,7 +204,7 @@ func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
||||||
|
|
||||||
// ReceiveGroupMessage is a callback function that processes GroupMessages from a given server
|
// ReceiveGroupMessage is a callback function that processes GroupMessages from a given server
|
||||||
func (cp *cwtchPeer) ReceiveGroupMessage(server string, gm *protocol.GroupMessage) {
|
func (cp *cwtchPeer) ReceiveGroupMessage(server string, gm *protocol.GroupMessage) {
|
||||||
cp.Profile.AttemptDecryption(gm.Ciphertext, gm.Signature)
|
cp.profile.AttemptDecryption(gm.Ciphertext, gm.Signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinServer manages a new server connection with the given onion address
|
// JoinServer manages a new server connection with the given onion address
|
||||||
|
@ -307,7 +214,7 @@ func (cp *cwtchPeer) JoinServer(onion string) {
|
||||||
|
|
||||||
// SendMessageToGroup attemps to sent the given message to the given group id.
|
// SendMessageToGroup attemps to sent the given message to the given group id.
|
||||||
func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
|
func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
|
||||||
group := cp.Profile.GetGroupByGroupID(groupid)
|
group := cp.profile.GetGroupByGroupID(groupid)
|
||||||
if group == nil {
|
if group == nil {
|
||||||
return errors.New("group does not exist")
|
return errors.New("group does not exist")
|
||||||
}
|
}
|
||||||
|
@ -315,7 +222,7 @@ func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
|
||||||
if psc == nil {
|
if psc == nil {
|
||||||
return errors.New("could not find server connection to send message to")
|
return errors.New("could not find server connection to send message to")
|
||||||
}
|
}
|
||||||
ct, sig, err := cp.Profile.EncryptMessageToGroup(message, groupid)
|
ct, sig, err := cp.profile.EncryptMessageToGroup(message, groupid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -339,7 +246,7 @@ func (cp *cwtchPeer) GetServers() map[string]connections.ConnectionState {
|
||||||
|
|
||||||
// TrustPeer sets an existing peer relationship to trusted
|
// TrustPeer sets an existing peer relationship to trusted
|
||||||
func (cp *cwtchPeer) TrustPeer(peer string) error {
|
func (cp *cwtchPeer) TrustPeer(peer string) error {
|
||||||
err := cp.Profile.TrustPeer(peer)
|
err := cp.profile.TrustPeer(peer)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cp.PeerWithOnion(peer)
|
cp.PeerWithOnion(peer)
|
||||||
}
|
}
|
||||||
|
@ -348,24 +255,24 @@ func (cp *cwtchPeer) TrustPeer(peer string) error {
|
||||||
|
|
||||||
// BlockPeer blocks an existing peer relationship.
|
// BlockPeer blocks an existing peer relationship.
|
||||||
func (cp *cwtchPeer) BlockPeer(peer string) error {
|
func (cp *cwtchPeer) BlockPeer(peer string) error {
|
||||||
err := cp.Profile.BlockPeer(peer)
|
err := cp.profile.BlockPeer(peer)
|
||||||
cp.connectionsManager.ClosePeerConnection(peer)
|
cp.connectionsManager.ClosePeerConnection(peer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptInvite accepts a given existing group invite
|
// AcceptInvite accepts a given existing group invite
|
||||||
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
|
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
|
||||||
return cp.Profile.AcceptInvite(groupID)
|
return cp.profile.AcceptInvite(groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RejectInvite rejects a given group invite.
|
// RejectInvite rejects a given group invite.
|
||||||
func (cp *cwtchPeer) RejectInvite(groupID string) {
|
func (cp *cwtchPeer) RejectInvite(groupID string) {
|
||||||
cp.Profile.RejectInvite(groupID)
|
cp.profile.RejectInvite(groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LookupContact returns that a contact is known and allowed to communicate for all cases.
|
// LookupContact returns that a contact is known and allowed to communicate for all cases.
|
||||||
func (cp *cwtchPeer) LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
|
func (cp *cwtchPeer) LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
|
||||||
blocked := cp.Profile.IsBlocked(hostname)
|
blocked := cp.profile.IsBlocked(hostname)
|
||||||
return !blocked, true
|
return !blocked, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,7 +284,7 @@ func (cp *cwtchPeer) ContactRequest(name string, message string) string {
|
||||||
// Listen sets up an onion listener to process incoming cwtch messages
|
// Listen sets up an onion listener to process incoming cwtch messages
|
||||||
func (cp *cwtchPeer) Listen() error {
|
func (cp *cwtchPeer) Listen() error {
|
||||||
cwtchpeer := new(application.RicochetApplication)
|
cwtchpeer := new(application.RicochetApplication)
|
||||||
l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", cp.Profile.OnionPrivateKey, 9878)
|
l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", cp.profile.OnionPrivateKey, 9878)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -394,7 +301,7 @@ func (cp *cwtchPeer) Listen() error {
|
||||||
return cpc
|
return cpc
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
cwtchpeer.Init(cp.Profile.Name, cp.Profile.OnionPrivateKey, af, cp)
|
cwtchpeer.Init(cp.profile.Name, cp.profile.OnionPrivateKey, af, cp)
|
||||||
log.Printf("Running cwtch peer on %v", l.Addr().String())
|
log.Printf("Running cwtch peer on %v", l.Addr().String())
|
||||||
cp.app = cwtchpeer
|
cp.app = cwtchpeer
|
||||||
cwtchpeer.Run(l)
|
cwtchpeer.Run(l)
|
||||||
|
@ -405,6 +312,7 @@ func (cp *cwtchPeer) Listen() error {
|
||||||
func (cp *cwtchPeer) Shutdown() {
|
func (cp *cwtchPeer) Shutdown() {
|
||||||
cp.connectionsManager.Shutdown()
|
cp.connectionsManager.Shutdown()
|
||||||
cp.app.Shutdown()
|
cp.app.Shutdown()
|
||||||
|
cp.profile.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CwtchPeerInstance encapsulates incoming peer connections
|
// CwtchPeerInstance encapsulates incoming peer connections
|
||||||
|
@ -428,17 +336,17 @@ type CwtchPeerHandler struct {
|
||||||
// ClientIdentity handles incoming ClientIdentity packets
|
// ClientIdentity handles incoming ClientIdentity packets
|
||||||
func (cph *CwtchPeerHandler) ClientIdentity(ci *protocol.CwtchIdentity) {
|
func (cph *CwtchPeerHandler) ClientIdentity(ci *protocol.CwtchIdentity) {
|
||||||
log.Printf("Received Client Identity from %v %v\n", cph.Onion, ci.String())
|
log.Printf("Received Client Identity from %v %v\n", cph.Onion, ci.String())
|
||||||
cph.Peer.Profile.AddCwtchIdentity(cph.Onion, ci)
|
cph.Peer.profile.AddCwtchIdentity(cph.Onion, ci)
|
||||||
cph.Peer.Save()
|
cph.Peer.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleGroupInvite handles incoming GroupInvites
|
// HandleGroupInvite handles incoming GroupInvites
|
||||||
func (cph *CwtchPeerHandler) HandleGroupInvite(gci *protocol.GroupChatInvite) {
|
func (cph *CwtchPeerHandler) HandleGroupInvite(gci *protocol.GroupChatInvite) {
|
||||||
log.Printf("Received GroupID from %v %v\n", cph.Onion, gci.String())
|
log.Printf("Received GroupID from %v %v\n", cph.Onion, gci.String())
|
||||||
cph.Peer.Profile.ProcessInvite(gci, cph.Onion)
|
cph.Peer.profile.ProcessInvite(gci, cph.Onion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientIdentityPacket returns our ClientIdentity packet so it can be sent to the connected peer.
|
// GetClientIdentityPacket returns our ClientIdentity packet so it can be sent to the connected peer.
|
||||||
func (cph *CwtchPeerHandler) GetClientIdentityPacket() []byte {
|
func (cph *CwtchPeerHandler) GetClientIdentityPacket() []byte {
|
||||||
return cph.Peer.Profile.GetCwtchIdentityPacket()
|
return cph.Peer.profile.GetCwtchIdentityPacket()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,29 +5,25 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCwtchPeerGenerate(t *testing.T) {
|
func TestCwtchPeerGenerate(t *testing.T) {
|
||||||
|
alice := NewCwtchPeer("alice")
|
||||||
alice := NewCwtchPeer("alice", "testpass", "./alice.json")
|
err := alice.Save()
|
||||||
alice.Save()
|
if err == nil {
|
||||||
|
t.Errorf("newly generate peer should not be able to save")
|
||||||
aliceLoaded, err := LoadCwtchPeer("./alice.json", "testpass")
|
|
||||||
if err != nil || aliceLoaded.GetProfile().Name != "alice" {
|
|
||||||
t.Errorf("something went wrong saving and loading profiles %v %v", err, aliceLoaded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groupID, _, _ := aliceLoaded.StartGroup("test.server")
|
groupID, _, _ := alice.StartGroup("test.server")
|
||||||
exportedGroup, _ := aliceLoaded.ExportGroup(groupID)
|
exportedGroup, _ := alice.ExportGroup(groupID)
|
||||||
t.Logf("Exported Group: %v from %v", exportedGroup, aliceLoaded.GetProfile().Onion)
|
t.Logf("Exported Group: %v from %v", exportedGroup, alice.GetProfile().Onion)
|
||||||
|
|
||||||
importedGroupID, err := alice.ImportGroup(exportedGroup)
|
importedGroupID, err := alice.ImportGroup(exportedGroup)
|
||||||
group := alice.GetGroup(importedGroupID)
|
group := alice.GetGroup(importedGroupID)
|
||||||
t.Logf("Imported Group: %v, err := %v %v", group, err, importedGroupID)
|
t.Logf("Imported Group: %v, err := %v %v", group, err, importedGroupID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrustPeer(t *testing.T) {
|
func TestTrustPeer(t *testing.T) {
|
||||||
groupName := "test.server"
|
groupName := "test.server"
|
||||||
alice := NewCwtchPeer("alice", "alicepass", "")
|
alice := NewCwtchPeer("alice")
|
||||||
bob := NewCwtchPeer("bob", "bobpass", "")
|
bob := NewCwtchPeer("bob")
|
||||||
|
|
||||||
bobOnion := bob.GetProfile().Onion
|
bobOnion := bob.GetProfile().Onion
|
||||||
aliceOnion := alice.GetProfile().Onion
|
aliceOnion := alice.GetProfile().Onion
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
mrand "math/rand"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profile Storage stores profileGroups and supports storing "secret" profileGroups in a *deniable* manor
|
||||||
|
// Given preassigned block size and number of blocks the system generates random fillings for all slots
|
||||||
|
// slots 0 to n/2 - 1 are for public profileGroups and n/2 to n-1 are for "secret" profileGroups, and use different keys
|
||||||
|
// Any active profileGroups when saved are padded with random to the block size
|
||||||
|
// Thus on inspection it should be impossible to determine if the secret profileGroups are being used or not
|
||||||
|
|
||||||
|
// The reason we need to formally segment "main" and "deniable" profileGroups is that if they are mixed (and there are
|
||||||
|
// deniable profileGroups with more than one password) it can quikcly lead to a situation where if not all profileGroups are loaded
|
||||||
|
// and yet the user wants to add a new one, the system will have no way to determine if a slot is in use or not
|
||||||
|
|
||||||
|
// Profile block size
|
||||||
|
// Salt 128
|
||||||
|
// PublicProfile = ~106 (name is variable, pegged at 16 here)
|
||||||
|
// Group = 170
|
||||||
|
//
|
||||||
|
// PrivateProfile = 320 + Profile(106) ... 426
|
||||||
|
// + map[onion 56] PubProfile 106 = 162
|
||||||
|
// + map[groupid 16] Group 170 = 186
|
||||||
|
//
|
||||||
|
// Assume 256 peers and 256 groups max [weak] = 89514
|
||||||
|
// Hand wavy 1.5 multiplier for conversion to json
|
||||||
|
// = 113,450 bytes per peer block... * ignoring timelines...
|
||||||
|
|
||||||
|
// This is still limiting of some uses cases like massive distribution lists mimicing twitter behaviour
|
||||||
|
// where one peer can have up to millions of followers
|
||||||
|
|
||||||
|
// We could enable profileGroups to fill multiple blocks, but this may degrade secret profile deniability ?
|
||||||
|
|
||||||
|
// ProfileGroupID is used to select what division of the profiles storage to store a profile in
|
||||||
|
// They are all the same but we encourage thinking of group1 as a master/main group and the rest as more deniable
|
||||||
|
// Also depends on how many groups you configure your store to use
|
||||||
|
type ProfileGroupID int
|
||||||
|
|
||||||
|
// Suggested settings for initializing a ProfileStore. Not required to use
|
||||||
|
const (
|
||||||
|
HeaderSize = secretbox.Overhead + 24 + 128 // overhead + nounce + salt
|
||||||
|
DataBlockSizeSmall = 113450
|
||||||
|
DataBlockSizeMedium = DataBlockSizeSmall * 4
|
||||||
|
DataBlockSizeLarge = DataBlockSizeMedium * 4
|
||||||
|
NumProfileBlocks = 32 // means each small store is about 2.2 MB, medium is 10 MB, and large is 40 MB
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usable names for profileStore divisions
|
||||||
|
const (
|
||||||
|
GroupMaster ProfileGroupID = iota
|
||||||
|
GroupDeniable1
|
||||||
|
GroupDeniable2
|
||||||
|
GroupDeniable3
|
||||||
|
GroupDeniable4
|
||||||
|
GroupDeniable5
|
||||||
|
)
|
||||||
|
|
||||||
|
type profileGroup struct {
|
||||||
|
profiles []*model.Profile
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type profileStore struct {
|
||||||
|
dataBlocksize int
|
||||||
|
fullBlocksize int
|
||||||
|
numBlocks int
|
||||||
|
numGroups int
|
||||||
|
filename string
|
||||||
|
|
||||||
|
profileGroups []profileGroup
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileStore is a storage system for profiles which can offer deniability.
|
||||||
|
// It's main purpose is to give peer's profiles persistence
|
||||||
|
// It's secondard use is the ability to support deniable profiles. If an app uses a store with more than 1 division
|
||||||
|
// it should offer no way to prove that those divisions are or are not in use
|
||||||
|
type ProfileStore interface {
|
||||||
|
InitializeProfileGroup(groupid ProfileGroupID, password string) ([]*model.Profile, error)
|
||||||
|
AddProfile(groupid ProfileGroupID, profile *model.Profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProfileStore creates a new storage class and file for storing profile data
|
||||||
|
// block size should be as big as you can image a block ever needing to be, there are no current mechanisms for resizing
|
||||||
|
// numBlocks should be even, first half for profileGroups and second half for deniable profileGroups
|
||||||
|
func NewProfileStore(filepath string, blocksize int, numBlocks int, numGroups int) ProfileStore {
|
||||||
|
ps := new(profileStore)
|
||||||
|
ps.dataBlocksize = blocksize
|
||||||
|
ps.fullBlocksize = ps.dataBlocksize + HeaderSize
|
||||||
|
ps.numBlocks = numBlocks
|
||||||
|
ps.numGroups = numGroups
|
||||||
|
|
||||||
|
ps.profileGroups = make([]profileGroup, numGroups)
|
||||||
|
|
||||||
|
ps.filename = filepath
|
||||||
|
if _, err := os.Stat(ps.filename); os.IsNotExist(err) {
|
||||||
|
err := ps.generateFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func randStringBytes(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[mrand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateFile creates a storage backing for a profileStore in a file
|
||||||
|
// format is [rand*blockSize]*numBlocks
|
||||||
|
// which yeilds a file of numBlocks blocks of initially random data of blockSize
|
||||||
|
// As profileGroups are created the blocks can be populated in a semi deniable of use fashion
|
||||||
|
// (the fact the file exists indicates at least 1 profile, but no way beyond that to determine exactly how many)
|
||||||
|
func (ps *profileStore) generateFile() error {
|
||||||
|
ps.mutex.Lock()
|
||||||
|
|
||||||
|
file, err := os.OpenFile(ps.filename, os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error opening profile file for writting: %v", err)
|
||||||
|
ps.mutex.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and write NumProfileBlocks blocks
|
||||||
|
for i := 0; i < ps.numBlocks; i++ {
|
||||||
|
padding := make([]byte, ps.fullBlocksize)
|
||||||
|
rand.Read(padding)
|
||||||
|
file.Write(padding[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
ps.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeProfileGroup will set a password, attempt to load profile in that group, and then allow new profiles to be
|
||||||
|
// added to the group and saved under it
|
||||||
|
func (ps *profileStore) InitializeProfileGroup(groupid ProfileGroupID, password string) ([]*model.Profile, error) {
|
||||||
|
ps.mutex.Lock()
|
||||||
|
|
||||||
|
pg := &ps.profileGroups[groupid]
|
||||||
|
|
||||||
|
if len(pg.profiles) != 0 {
|
||||||
|
return nil, errors.New("ProfileGroup already has loaded profiles, cannot reinitialize")
|
||||||
|
}
|
||||||
|
|
||||||
|
pg.password = password
|
||||||
|
ps.mutex.Unlock()
|
||||||
|
err := ps.attemptLoad(groupid)
|
||||||
|
return ps.profileGroups[groupid].profiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *profileStore) getSaveFn(groupid ProfileGroupID, slot int) func() error {
|
||||||
|
return func() error {
|
||||||
|
ps.mutex.Lock()
|
||||||
|
defer ps.mutex.Unlock()
|
||||||
|
|
||||||
|
file, err := os.OpenFile(ps.filename, os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error opening ProfileStore file for saving: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
absoluteSlot := (ps.numBlocks/ps.numGroups)*int(groupid) + slot
|
||||||
|
|
||||||
|
_, err = file.Seek(int64(ps.fullBlocksize*absoluteSlot), 0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Seek error: %v\n", err)
|
||||||
|
}
|
||||||
|
encryptedByte := ps.profileGroups[groupid].profiles[slot].EncryptProfile(ps.dataBlocksize)
|
||||||
|
_, err = file.Write(encryptedByte)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error writting to profile store file: %v\n", err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *profileStore) attemptLoad(groupid ProfileGroupID) error {
|
||||||
|
ps.mutex.Lock()
|
||||||
|
defer ps.mutex.Unlock()
|
||||||
|
|
||||||
|
file, err := os.Open(ps.filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error opening profile store file: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Seek(int64(ps.fullBlocksize*(int(groupid)*(ps.numBlocks/ps.numGroups))), 0)
|
||||||
|
for i := 0; i < ps.numBlocks/ps.numGroups; i++ {
|
||||||
|
encryptedBytes := make([]byte, ps.fullBlocksize)
|
||||||
|
_, err := file.Read(encryptedBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading from profile store file: %v", err)
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profile := model.AttemptLoadProfile(encryptedBytes[:], ps.profileGroups[groupid].password, ps.getSaveFn(groupid, i))
|
||||||
|
if profile == nil {
|
||||||
|
// no more profiles to load
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ps.profileGroups[groupid].profiles = append(ps.profileGroups[groupid].profiles, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createKey derives a key and salt for use in encrypting cwtchPeers
|
||||||
|
func createKey(password string) ([32]byte, [128]byte) {
|
||||||
|
var salt [128]byte
|
||||||
|
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
|
||||||
|
|
||||||
|
var dkr [32]byte
|
||||||
|
copy(dkr[:], dk)
|
||||||
|
return dkr, salt
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddProfile adds a profile to a group, and wires it in to be Save()able: gives it a key, salt and save function
|
||||||
|
// then it is saved
|
||||||
|
func (ps *profileStore) AddProfile(groupid ProfileGroupID, profile *model.Profile) {
|
||||||
|
ps.mutex.Lock()
|
||||||
|
key, salt := createKey(ps.profileGroups[groupid].password)
|
||||||
|
profile.OnProfileStoreAdd(key, salt, ps.getSaveFn(groupid, len(ps.profileGroups[groupid].profiles)))
|
||||||
|
ps.profileGroups[groupid].profiles = append(ps.profileGroups[groupid].profiles, profile)
|
||||||
|
ps.mutex.Unlock()
|
||||||
|
profile.Save()
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PasswordMaster = "asdf1234"
|
||||||
|
DeniablePassword = "qwerty567"
|
||||||
|
ProfilesFile = "profiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
func statFileTest(t *testing.T) {
|
||||||
|
fi, err := os.Stat(ProfilesFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("no profile file: %v", err)
|
||||||
|
t.Errorf("Errors stating profile file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Size() != (DataBlockSizeSmall+HeaderSize)*6 {
|
||||||
|
fmt.Printf("Profile file size error, expected: %v actual: %v\n", (DataBlockSizeSmall+HeaderSize)*6, fi.Size())
|
||||||
|
t.Errorf("Profile file size error, expected: %v actual: %v", (DataBlockSizeSmall+HeaderSize)*6, fi.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProfileStore(t *testing.T) {
|
||||||
|
os.Remove(ProfilesFile)
|
||||||
|
ps := NewProfileStore(ProfilesFile, DataBlockSizeSmall, 6, 2)
|
||||||
|
|
||||||
|
statFileTest(t)
|
||||||
|
|
||||||
|
profiles, err := ps.InitializeProfileGroup(GroupMaster, PasswordMaster)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error Initializing master profile group: %v", err)
|
||||||
|
}
|
||||||
|
if len(profiles) > 0 {
|
||||||
|
t.Errorf("Somehow profiles were loaded from a expected blank group")
|
||||||
|
}
|
||||||
|
|
||||||
|
statFileTest(t)
|
||||||
|
|
||||||
|
alice := model.GenerateNewProfile("Alice")
|
||||||
|
|
||||||
|
err = alice.Save()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error on trying to save profile before added to ProfileStore")
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.AddProfile(GroupMaster, alice)
|
||||||
|
|
||||||
|
// Redundtant, but testing path
|
||||||
|
err = alice.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error saving profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statFileTest(t)
|
||||||
|
|
||||||
|
carol := model.GenerateNewProfile("Carol")
|
||||||
|
ps.AddProfile(GroupMaster, carol)
|
||||||
|
|
||||||
|
ps.InitializeProfileGroup(GroupDeniable1, DeniablePassword)
|
||||||
|
secretBob := model.GenerateNewProfile("Secret Bob")
|
||||||
|
ps.AddProfile(GroupDeniable1, secretBob)
|
||||||
|
|
||||||
|
// Confirm loading works
|
||||||
|
psL := NewProfileStore(ProfilesFile, DataBlockSizeSmall, 6, 2)
|
||||||
|
profilesL, err := psL.InitializeProfileGroup(GroupMaster, PasswordMaster)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error loading master profile group: %v", err)
|
||||||
|
}
|
||||||
|
if len(profilesL) != 2 {
|
||||||
|
t.Errorf("Error loading profiles: Expected: 2, Actual: %v", len(profilesL))
|
||||||
|
}
|
||||||
|
|
||||||
|
p1 := profilesL[0]
|
||||||
|
|
||||||
|
if p1.Name != alice.Name {
|
||||||
|
t.Errorf("Error loading profiles name expected: %v Actual: %v", alice.Name, p1.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
deniableProfiles, err := psL.InitializeProfileGroup(GroupDeniable1, DeniablePassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error loading deniable profiles: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deniableProfiles) != 1 {
|
||||||
|
t.Errorf("Error loading deniable profiles: expected: 1, actual: %v", len(deniableProfiles))
|
||||||
|
} else {
|
||||||
|
if deniableProfiles[0].Name != secretBob.Name {
|
||||||
|
t.Errorf("Error loading secret Bob profile: expected: '%v' actaul: '%v'", secretBob.Name, deniableProfiles[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -84,7 +84,7 @@ func serverCheck(t *testing.T, serverAddr string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForPeerConnection(t *testing.T, peer peer.CwtchPeerInterface, server string) {
|
func waitForPeerConnection(t *testing.T, peer peer.CwtchPeer, server string) {
|
||||||
for {
|
for {
|
||||||
servers := peer.GetServers()
|
servers := peer.GetServers()
|
||||||
state, ok := servers[server]
|
state, ok := servers[server]
|
||||||
|
@ -143,17 +143,17 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
// ***** Peer setup *****
|
// ***** Peer setup *****
|
||||||
|
|
||||||
fmt.Println("Creating Alice...")
|
fmt.Println("Creating Alice...")
|
||||||
alice := peer.NewCwtchPeer("Alice", "alicepass", "")
|
alice := peer.NewCwtchPeer("Alice")
|
||||||
go alice.Listen()
|
go alice.Listen()
|
||||||
fmt.Println("Alice created:", alice.GetProfile().Onion)
|
fmt.Println("Alice created:", alice.GetProfile().Onion)
|
||||||
|
|
||||||
fmt.Println("Creating Bob...")
|
fmt.Println("Creating Bob...")
|
||||||
bob := peer.NewCwtchPeer("Bob", "bobpass", "")
|
bob := peer.NewCwtchPeer("Bob")
|
||||||
go bob.Listen()
|
go bob.Listen()
|
||||||
fmt.Println("Bob created:", bob.GetProfile().Onion)
|
fmt.Println("Bob created:", bob.GetProfile().Onion)
|
||||||
|
|
||||||
fmt.Println("Creating Carol...")
|
fmt.Println("Creating Carol...")
|
||||||
carol := peer.NewCwtchPeer("Carol", "carolpass", "")
|
carol := peer.NewCwtchPeer("Carol")
|
||||||
go carol.Listen()
|
go carol.Listen()
|
||||||
fmt.Println("Carol created:", carol.GetProfile().Onion)
|
fmt.Println("Carol created:", carol.GetProfile().Onion)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue