Compare commits

...

2 Commits

10 changed files with 761 additions and 300 deletions

2
.gitignore vendored
View File

@ -3,5 +3,3 @@
*private_key*
*.messages
*.test
*/*test_*
*/*_test*

View File

@ -2,49 +2,58 @@ package app
import (
"cwtch.im/cwtch/connectivity/tor"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/peer"
"errors"
"cwtch.im/cwtch/storage"
"fmt"
"log"
"os"
"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.
type Application struct {
Peer peer.CwtchPeerInterface
TorManager *tor.Manager
directory string
type application struct {
peers map[string]peer.CwtchPeer
profileStore storage.ProfileStore
torManager *tor.Manager
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) (int, error)
InitDeniableProfiles(password string) (int, error)
CreatePeer(name string) (peer.CwtchPeer, error)
GetPeer(onion string) peer.CwtchPeer
ListPeers() map[string]string
Shutdown()
}
// 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)
app := &Application{Peer: nil, directory: appDirectory}
app := &application{peers: make(map[string]peer.CwtchPeer), directory: appDirectory}
os.MkdirAll(path.Join(appDirectory, "tor"), 0700)
err := app.startTor(torPath)
if err != nil {
return nil, err
}
app.profileStore = storage.NewProfileStore(path.Join(appDirectory, ProfileFileName), storage.DataBlockSizeMedium, storage.NumProfileBlocks, 2)
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
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 $app.directory/torrc file
// SOCKSPort socksPort
@ -64,35 +73,93 @@ func (app *Application) startTor(torPath string) error {
if err != nil {
return err
}
app.TorManager = tm
app.torManager = tm
return nil
}
// SetProfile loads an existing profile from the given filename.
func (app *Application) SetProfile(filename string, password string) error {
if app.Peer == nil {
profile, err := peer.LoadCwtchPeer(path.Join(app.directory, filename), password)
if err != nil {
return err
}
app.Peer = profile
return app.startPeer()
// InitProfiles will attempt to load the normal portion of profile storage with the supplied password, returning number of profiles loaded
func (app *application) InitProfiles(password string) (int, error) {
profiles, err := app.profileStore.InitializeProfileGroup(storage.GroupMaster, password)
if err != nil {
return 0, nil
}
return errors.New("profile is already loaded, to load a different profile you will need to restart the application")
n := app.loadProfiles(profiles)
return n, nil
}
func (app *Application) startPeer() error {
// InitDeniableProfiles will attempt to load the deniable portion of profile storage with the supplied password, returning number of profiles loaded
func (app *application) InitDeniableProfiles(password string) (int, error) {
profiles, err := app.profileStore.InitializeProfileGroup(storage.GroupDeniable1, password)
if err != nil {
return 0, err
}
n := app.loadProfiles(profiles)
return n, nil
}
func (app *application) loadProfiles(profiles []*model.Profile) int {
app.mutex.Lock()
count := 0
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)
count++
}
app.mutex.Unlock()
return count
}
// NewProfile creates a new cwtchPeer with a given name.
func (app *application) CreatePeer(name string) (peer.CwtchPeer, error) {
app.mutex.Lock()
p := peer.NewCwtchPeer(name)
err := app.profileStore.AddProfile(storage.GroupMaster, p.GetProfile())
if err != nil {
log.Printf("Could not add profile to profile store: %v", err)
return nil, err
}
app.peers[p.GetProfile().Onion] = p
app.startPeer(p)
return p, nil
}
func (app *application) startPeer(peer peer.CwtchPeer) {
go func() {
e := app.Peer.Listen()
e := peer.Listen()
if e != nil {
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
}
// PeerRequest attempts to setup peer relationship with the given onion address.`
func (app *Application) PeerRequest(onion string) {
app.Peer.PeerWithOnion(onion)
// Shutdown shutsdown all peers of an app and then the tormanager
func (app *application) Shutdown() {
for _, peer := range app.peers {
peer.Shutdown()
}
app.torManager.Shutdown()
}

View File

@ -9,6 +9,9 @@ import (
"time"
"bytes"
peer2 "cwtch.im/cwtch/peer"
"cwtch.im/cwtch/peer/connections"
"errors"
"golang.org/x/crypto/ssh/terminal"
"log"
"os"
@ -18,32 +21,37 @@ import (
"syscall"
)
var app *app2.Application
var app app2.Application
var peer peer2.CwtchPeer
var suggestions = []prompt.Suggest{
{Text: "new-profile", Description: "create a new profile in ~/.cwtch/$USERNAME.json"},
{Text: "load-profile", Description: "load a new profile"},
{Text: "new-profile", Description: "create 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: "info", Description: "show user info"},
{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: "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-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: "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{
"new-profile": "new-profile [name]",
"load-profile": "load-profile [filename]",
"load-profiles": "",
"list-profiles": "",
"select-profile": "select-profile [onion]",
"quit": "",
"servers": "",
"peers": "",
@ -70,16 +78,16 @@ func completer(d prompt.Document) []prompt.Suggest {
return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true)
}
if app.Peer == nil {
if peer == nil {
return s
}
w := d.CurrentLine()
if strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") || strings.HasPrefix(w, "export-group") {
s = []prompt.Suggest{}
groups := app.Peer.GetGroups()
groups := peer.GetGroups()
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})
}
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") {
s = []prompt.Suggest{}
contacts := app.Peer.GetContacts()
contacts := peer.GetContacts()
for _, onion := range contacts {
contact := app.Peer.GetContact(onion)
contact := peer.GetContact(onion)
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
@ -99,17 +107,17 @@ func completer(d prompt.Document) []prompt.Suggest {
if d.FindStartOfPreviousWordWithSpace() == 0 {
s = []prompt.Suggest{}
contacts := app.Peer.GetContacts()
contacts := peer.GetContacts()
for _, onion := range contacts {
contact := app.Peer.GetContact(onion)
contact := peer.GetContact(onion)
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
s = []prompt.Suggest{}
groups := app.Peer.GetGroups()
groups := peer.GetGroups()
for _, groupID := range groups {
group := app.Peer.GetGroup(groupID)
group := peer.GetGroup(groupID)
if group.Owner == "self" {
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") {
s = []prompt.Suggest{}
groups := app.Peer.GetGroups()
groups := peer.GetGroups()
for _, groupID := range groups {
group := app.Peer.GetGroup(groupID)
group := peer.GetGroup(groupID)
if group.Accepted == false {
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)
}
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
}
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() {
cwtch :=
@ -184,15 +229,30 @@ func main() {
log.Fatalf("\nError: could not load current user: %v\n", err)
}
app, err = app2.NewApp(path.Join(usr.HomeDir, ".cwtch"), torPath)
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.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 {
log.Fatalf("Error initializing application: %v", err)
}
var history []string
for !quit {
profile := "unset"
if app.Peer != nil {
profile = app.Peer.GetProfile().Name
if peer != nil {
profile = peer.GetProfile().Name
}
prmpt := fmt.Sprintf("cwtch [%v]> ", profile)
@ -203,128 +263,115 @@ func main() {
commands := strings.Split(text[0:], " ")
history = append(history, text)
if app.Peer == nil {
if commands[0] != "help" && commands[0] != "quit" && commands[0] != "new-profile" && commands[0] != "load-profile" {
fmt.Printf("Profile needs to be set\n")
if peer == nil {
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 with `select-profile` or `new-profile`\n")
continue
}
}
switch commands[0] {
case "quit":
if app.Peer != nil {
app.Peer.Save()
if peer != nil {
peer.Save()
}
quit = true
case "new-profile":
if len(commands) == 2 {
fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n")
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])
p, err := app.CreatePeer(commands[1])
if err != nil {
fmt.Printf("Error creating profile: %v\n", err)
} 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)
}
peer = p
}
} else {
fmt.Printf("Error creating NewProfile, usage: %s\n", usages["new-profile"])
}
case "load-profile":
case "load-profiles":
fmt.Print("Enter a password to decrypt the profiles: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
n, err := app.InitProfiles(string(bytePassword))
if err == nil {
fmt.Printf("\n%v profiles active now\n", n)
fmt.Printf("You should run `select-profile` to use a profile\n")
} else {
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 {
fmt.Print("Enter a password to decrypt the profile: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
err = app.SetProfile(commands[1]+".json", string(bytePassword))
if err == nil {
fmt.Printf("\nLoaded profile for %v\n", commands[1])
p := app.GetPeer(commands[1])
if p == nil {
fmt.Printf("Error: profile '%v' does not exist\n", commands[1])
} else {
fmt.Printf("\nError loading profile for %v: %v\n", commands[1], err)
peer = p
}
} 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":
if app.Peer != nil {
fmt.Printf("Address cwtch:%v\n", app.Peer.GetProfile().Onion)
if peer != nil {
fmt.Printf("Address cwtch:%v\n", peer.GetProfile().Onion)
} else {
fmt.Printf("Profile needs to be set\n")
}
case "invite":
if len(commands) == 2 {
fmt.Printf("Inviting cwtch:%v\n", commands[1])
app.PeerRequest(commands[1])
peer.PeerWithOnion(commands[1])
} else {
fmt.Printf("Error inviting peer, usage: %s\n", usages["invite"])
}
case "peers":
peers := app.Peer.GetPeers()
peers := peer.GetPeers()
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":
servers := app.Peer.GetServers()
servers := peer.GetServers()
for s, st := range servers {
fmt.Printf("Name: %v Status: %v\n", s, st)
}
case "contacts":
contacts := app.Peer.GetContacts()
contacts := peer.GetContacts()
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)
}
case "groups":
for _, gid := range app.Peer.GetGroups() {
g := app.Peer.GetGroup(gid)
for _, gid := range peer.GetGroups() {
g := peer.GetGroup(gid)
fmt.Printf("Group Id: %v Owner: %v Accepted:%v\n", gid, g.Owner, g.Accepted)
}
case "trust":
if len(commands) == 2 {
app.Peer.TrustPeer(commands[1])
peer.TrustPeer(commands[1])
} else {
fmt.Printf("Error trusting peer, usage: %s\n", usages["trust"])
}
case "block":
if len(commands) == 2 {
app.Peer.BlockPeer(commands[1])
peer.BlockPeer(commands[1])
} else {
fmt.Printf("Error blocking peer, usage: %s\n", usages["trust"])
}
case "accept-invite":
if len(commands) == 2 {
groupID := commands[1]
err := app.Peer.AcceptInvite(groupID)
err := peer.AcceptInvite(groupID)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
app.Peer.Save()
group := app.Peer.GetGroup(groupID)
peer.Save()
group := peer.GetGroup(groupID)
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
app.Peer.JoinServer(group.GroupServer)
peer.JoinServer(group.GroupServer)
}
}
} else {
@ -333,7 +380,7 @@ func main() {
case "invite-to-group":
if len(commands) == 3 {
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 {
fmt.Printf("Error: %v\n", err)
}
@ -343,15 +390,15 @@ func main() {
case "new-group":
if len(commands) == 2 && 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 {
fmt.Printf("New Group [%v] created for server %v\n", id, commands[1])
app.Peer.Save()
group := app.Peer.GetGroup(id)
peer.Save()
group := peer.GetGroup(id)
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
app.Peer.JoinServer(group.GroupServer)
peer.JoinServer(group.GroupServer)
}
} else {
fmt.Printf("Error creating new group: %v", err)
@ -362,7 +409,7 @@ func main() {
case "send":
if len(commands) > 2 {
message := strings.Join(commands[2:], " ")
err := app.Peer.SendMessageToGroup(commands[1], message)
err := peer.SendMessageToGroup(commands[1], message)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
@ -371,7 +418,7 @@ func main() {
}
case "timeline":
if len(commands) == 2 {
group := app.Peer.GetGroup(commands[1])
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
@ -382,12 +429,12 @@ func main() {
verified = "verified"
}
p := app.Peer.GetContact(m.PeerID)
p := peer.GetContact(m.PeerID)
name := "unknown"
if p != nil {
name = p.Name
} else if app.Peer.GetProfile().Onion == m.PeerID {
name = app.Peer.GetProfile().Name
} else if peer.GetProfile().Onion == m.PeerID {
name = peer.GetProfile().Name
}
fmt.Printf("%v %v (%v): %v [%s]\n", m.Timestamp, name, m.PeerID, m.Message, verified)
@ -398,31 +445,31 @@ func main() {
}
case "export-group":
if len(commands) == 2 {
group := app.Peer.GetGroup(commands[1])
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
invite, _ := app.Peer.ExportGroup(commands[1])
invite, _ := peer.ExportGroup(commands[1])
fmt.Printf("Invite: %v\n", invite)
}
} else {
fmt.Printf("Error exporting group, usage: %s\n", usages["export-group"])
}
case "save":
app.Peer.Save()
peer.Save()
case "help":
for _, command := range suggestions {
fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text])
}
case "sendlots":
if len(commands) == 2 {
group := app.Peer.GetGroup(commands[1])
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
for i := 0; i < 100; 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 {
fmt.Printf("could not send message %v because %v\n", i, err)
}
@ -436,7 +483,7 @@ func main() {
for i := 0; i < 100; i++ {
found := false
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
latency := m.Received.Sub(m.Timestamp)
fmt.Printf("Latency for Message %v was %v\n", i, latency)
@ -459,15 +506,12 @@ func main() {
}
}
}
if app.Peer != nil {
app.Peer.Save()
if peer != nil {
peer.Save()
}
}
if app.TorManager != nil {
fmt.Println("Shutting down Tor process...")
app.TorManager.Shutdown()
}
app.Shutdown()
os.Exit(0)
}

View File

@ -1,14 +1,19 @@
package model
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"cwtch.im/cwtch/protocol"
"encoding/asn1"
"encoding/json"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"sync"
"time"
@ -31,12 +36,93 @@ type Profile struct {
OnionPrivateKey *rsa.PrivateKey
Groups map[string]*Group
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.
func GenerateNewProfile(name string) *Profile {
p := new(Profile)
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)
p.Ed25519PublicKey = pub
p.Ed25519PrivateKey = priv

View File

@ -16,3 +16,8 @@ const (
FAILED
KILLED
)
var (
// ConnectionStateName allows conversaion of states to their string representations
ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Failed", "Killed"}
)

View File

@ -1,27 +1,18 @@
package peer
import (
"crypto/rand"
"crypto/rsa"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/peer/connections"
"cwtch.im/cwtch/peer/peer"
"cwtch.im/cwtch/protocol"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
"github.com/golang/protobuf/proto"
"github.com/ulule/deepcopier"
"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"
"strings"
"sync"
@ -30,19 +21,16 @@ import (
// cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch Peer
type cwtchPeer struct {
connection.AutoConnectionHandler
Profile *model.Profile
profile *model.Profile
app *application.RicochetApplication
mutex sync.Mutex
Log chan string `json:"-"`
Log chan string
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.
type CwtchPeerInterface interface {
type CwtchPeer interface {
Save() error
PeerWithOnion(string)
InviteOnionToGroup(string, string) error
@ -74,56 +62,6 @@ type CwtchPeerInterface interface {
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() {
cp.Log = make(chan string)
cp.connectionsManager = connections.NewConnectionsManager()
@ -131,13 +69,13 @@ func (cp *cwtchPeer) setup() {
go cp.connectionsManager.AttemptReconnections()
for onion, profile := range cp.Profile.Contacts {
for onion, profile := range cp.profile.Contacts {
if profile.Trusted && !profile.Blocked {
cp.PeerWithOnion(onion)
}
}
for _, group := range cp.Profile.Groups {
for _, group := range cp.profile.Groups {
if group.Accepted || group.Owner == "self" {
cp.JoinServer(group.GroupServer)
}
@ -145,55 +83,24 @@ func (cp *cwtchPeer) setup() {
}
// 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.profilefile = profilefile
cp.Profile = model.GenerateNewProfile(name)
cp.profile = model.GenerateNewProfile(name)
cp.setup()
key, salt := createKey(password)
cp.key = key
cp.salt = salt
return cp
}
// Save saves the cwtchPeer profile state to a file.
func (cp *cwtchPeer) Save() error {
cp.mutex.Lock()
encryptedbytes := encryptProfile(cp, cp.key)
// the salt for the derived key is appended to the front of the file
encryptedbytes = append(cp.salt[:], encryptedbytes...)
err := ioutil.WriteFile(cp.profilefile, encryptedbytes, 0600)
cp.mutex.Unlock()
return err
// InitFromProfile starts a cwtchPeer from a loaded Profile
func InitFromProfile(p *model.Profile) CwtchPeer {
cp := new(cwtchPeer)
cp.profile = p
cp.setup()
return cp
}
// LoadCwtchPeer loads an existing cwtchPeer from a file.
func LoadCwtchPeer(profilefile string, password string) (CwtchPeerInterface, error) {
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
func (cp *cwtchPeer) Save() error {
return cp.profile.Save()
}
// 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 {
edpk := ed25519.PublicKey(pk)
onion := exportedInvite[5:21]
cp.Profile.AddContact(onion, &model.PublicProfile{Name: "", Ed25519PublicKey: edpk, Trusted: true, Blocked: false, Onion: onion})
cp.Profile.ProcessInvite(cpp.GetGroupChatInvite(), onion)
cp.profile.AddContact(onion, &model.PublicProfile{Name: "", Ed25519PublicKey: edpk, Trusted: true, Blocked: false, Onion: onion})
cp.profile.ProcessInvite(cpp.GetGroupChatInvite(), onion)
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
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
group := cp.Profile.GetGroupByGroupID(groupID)
group := cp.profile.GetGroupByGroupID(groupID)
if group != nil {
invite, err := group.Invite()
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
}
}
@ -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.
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.
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.
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
func (cp *cwtchPeer) GetContacts() []string {
return cp.Profile.GetContacts()
return cp.profile.GetContacts()
}
// GetContact returns a given contact, nil is no such contact exists
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
contact, _ := cp.Profile.GetContact(onion)
contact, _ := cp.profile.GetContact(onion)
return contact
}
// 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.
func (cp *cwtchPeer) GetProfile() *model.Profile {
return cp.Profile
return cp.profile
}
// PeerWithOnion is the entry point for cwtchPeer relationships
func (cp *cwtchPeer) PeerWithOnion(onion string) {
cp.connectionsManager.ManagePeerConnection(onion, cp.Profile)
cp.connectionsManager.ManagePeerConnection(onion, cp.profile)
}
// InviteOnionToGroup kicks off the invite process
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
group := cp.Profile.GetGroupByGroupID(groupid)
group := cp.profile.GetGroupByGroupID(groupid)
if group != nil {
log.Printf("Constructing invite for group: %v\n", group)
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
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
@ -307,7 +214,7 @@ func (cp *cwtchPeer) JoinServer(onion string) {
// SendMessageToGroup attemps to sent the given message to the given group id.
func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
group := cp.Profile.GetGroupByGroupID(groupid)
group := cp.profile.GetGroupByGroupID(groupid)
if group == nil {
return errors.New("group does not exist")
}
@ -315,7 +222,7 @@ func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
if psc == nil {
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 {
return err
}
@ -339,7 +246,7 @@ func (cp *cwtchPeer) GetServers() map[string]connections.ConnectionState {
// TrustPeer sets an existing peer relationship to trusted
func (cp *cwtchPeer) TrustPeer(peer string) error {
err := cp.Profile.TrustPeer(peer)
err := cp.profile.TrustPeer(peer)
if err == nil {
cp.PeerWithOnion(peer)
}
@ -348,24 +255,24 @@ func (cp *cwtchPeer) TrustPeer(peer string) error {
// BlockPeer blocks an existing peer relationship.
func (cp *cwtchPeer) BlockPeer(peer string) error {
err := cp.Profile.BlockPeer(peer)
err := cp.profile.BlockPeer(peer)
cp.connectionsManager.ClosePeerConnection(peer)
return err
}
// AcceptInvite accepts a given existing group invite
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
return cp.Profile.AcceptInvite(groupID)
return cp.profile.AcceptInvite(groupID)
}
// RejectInvite rejects a given group invite.
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.
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
}
@ -377,7 +284,7 @@ func (cp *cwtchPeer) ContactRequest(name string, message string) string {
// Listen sets up an onion listener to process incoming cwtch messages
func (cp *cwtchPeer) Listen() error {
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 {
return err
@ -394,7 +301,7 @@ func (cp *cwtchPeer) Listen() error {
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())
cp.app = cwtchpeer
cwtchpeer.Run(l)
@ -405,6 +312,7 @@ func (cp *cwtchPeer) Listen() error {
func (cp *cwtchPeer) Shutdown() {
cp.connectionsManager.Shutdown()
cp.app.Shutdown()
cp.profile.Save()
}
// CwtchPeerInstance encapsulates incoming peer connections
@ -428,17 +336,17 @@ type CwtchPeerHandler struct {
// ClientIdentity handles incoming ClientIdentity packets
func (cph *CwtchPeerHandler) ClientIdentity(ci *protocol.CwtchIdentity) {
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()
}
// HandleGroupInvite handles incoming GroupInvites
func (cph *CwtchPeerHandler) HandleGroupInvite(gci *protocol.GroupChatInvite) {
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.
func (cph *CwtchPeerHandler) GetClientIdentityPacket() []byte {
return cph.Peer.Profile.GetCwtchIdentityPacket()
return cph.Peer.profile.GetCwtchIdentityPacket()
}

View File

@ -5,29 +5,25 @@ import (
)
func TestCwtchPeerGenerate(t *testing.T) {
alice := NewCwtchPeer("alice", "testpass", "./alice.json")
alice.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)
alice := NewCwtchPeer("alice")
err := alice.Save()
if err == nil {
t.Errorf("newly generate peer should not be able to save")
}
groupID, _, _ := aliceLoaded.StartGroup("test.server")
exportedGroup, _ := aliceLoaded.ExportGroup(groupID)
t.Logf("Exported Group: %v from %v", exportedGroup, aliceLoaded.GetProfile().Onion)
groupID, _, _ := alice.StartGroup("test.server")
exportedGroup, _ := alice.ExportGroup(groupID)
t.Logf("Exported Group: %v from %v", exportedGroup, alice.GetProfile().Onion)
importedGroupID, err := alice.ImportGroup(exportedGroup)
group := alice.GetGroup(importedGroupID)
t.Logf("Imported Group: %v, err := %v %v", group, err, importedGroupID)
}
func TestTrustPeer(t *testing.T) {
groupName := "test.server"
alice := NewCwtchPeer("alice", "alicepass", "")
bob := NewCwtchPeer("bob", "bobpass", "")
alice := NewCwtchPeer("alice")
bob := NewCwtchPeer("bob")
bobOnion := bob.GetProfile().Onion
aliceOnion := alice.GetProfile().Onion

250
storage/profile_store.go Normal file
View File

@ -0,0 +1,250 @@
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"
"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
)
// 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) error
}
// 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
}
// 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()
defer ps.mutex.Unlock()
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)
return err
}
// Generate and write NumProfileBlocks blocks
for i := 0; i < ps.numBlocks; i++ {
padding := make([]byte, ps.fullBlocksize)
_, err := rand.Read(padding)
if err != nil {
log.Printf("Error: could not read from Rand to fill profile padding: %v", err)
return err
}
file.Write(padding[:])
}
file.Close()
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 {
ps.mutex.Unlock()
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
}
defer file.Close()
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)
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)
}
return nil
}
// createKey derives a key and salt for use in encrypting cwtchPeers
func createKey(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
var dkr [32]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
fmt.Printf("Error: Could not read Rand for salt: %v", err)
return dkr, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
copy(dkr[:], dk)
return dkr, salt, nil
}
// 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) error {
ps.mutex.Lock()
key, salt, err := createKey(ps.profileGroups[groupid].password)
if err != nil {
return err
}
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()
return nil
}

View File

@ -0,0 +1,107 @@
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")
}
err = ps.AddProfile(GroupMaster, alice)
if err != nil {
t.Errorf("Error adding profile to profile store: %v", err)
}
// Redundtant, but testing path
err = alice.Save()
if err != nil {
t.Errorf("Error saving profile: %v", err)
}
statFileTest(t)
carol := model.GenerateNewProfile("Carol")
err = ps.AddProfile(GroupMaster, carol)
if err != nil {
t.Errorf("Error adding profile to profile store: %v", err)
}
ps.InitializeProfileGroup(GroupDeniable1, DeniablePassword)
secretBob := model.GenerateNewProfile("Secret Bob")
err = ps.AddProfile(GroupDeniable1, secretBob)
if err != nil {
t.Errorf("Error adding profile to profile store: %v", err)
}
// 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)
}
}
}

View File

@ -84,7 +84,7 @@ func serverCheck(t *testing.T, serverAddr string) bool {
return true
}
func waitForPeerConnection(t *testing.T, peer peer.CwtchPeerInterface, server string) {
func waitForPeerConnection(t *testing.T, peer peer.CwtchPeer, server string) {
for {
servers := peer.GetServers()
state, ok := servers[server]
@ -143,17 +143,17 @@ func TestCwtchPeerIntegration(t *testing.T) {
// ***** Peer setup *****
fmt.Println("Creating Alice...")
alice := peer.NewCwtchPeer("Alice", "alicepass", "")
alice := peer.NewCwtchPeer("Alice")
go alice.Listen()
fmt.Println("Alice created:", alice.GetProfile().Onion)
fmt.Println("Creating Bob...")
bob := peer.NewCwtchPeer("Bob", "bobpass", "")
bob := peer.NewCwtchPeer("Bob")
go bob.Listen()
fmt.Println("Bob created:", bob.GetProfile().Onion)
fmt.Println("Creating Carol...")
carol := peer.NewCwtchPeer("Carol", "carolpass", "")
carol := peer.NewCwtchPeer("Carol")
go carol.Listen()
fmt.Println("Carol created:", carol.GetProfile().Onion)